Browse Source

Logout implemented

pull/1/head
Sebastian 9 years ago
parent
commit
aaac4c284d
  1. 17
      src/Squidex.Infrastructure/Security/ExtendedClaimTypes.cs
  2. 19
      src/Squidex/.sass-lint.yml
  3. 2
      src/Squidex/Configurations/Constants.cs
  4. 10
      src/Squidex/Configurations/Identity/IdentityServices.cs
  5. 56
      src/Squidex/Configurations/Identity/IdentityUsage.cs
  6. 10
      src/Squidex/Configurations/Identity/LazyClientStore.cs
  7. 36
      src/Squidex/Modules/UI/Account/AccountController.cs
  8. 1
      src/Squidex/Properties/launchSettings.json
  9. 10
      src/Squidex/app/app.module.ts
  10. 14
      src/Squidex/app/app.routes.ts
  11. 1
      src/Squidex/app/components/auth/declarations.ts
  12. 5
      src/Squidex/app/components/auth/login-page.component.html
  13. 5
      src/Squidex/app/components/auth/logout-page.component.html
  14. 6
      src/Squidex/app/components/auth/logout-page.component.ts
  15. 4
      src/Squidex/app/components/auth/module.ts
  16. 2
      src/Squidex/app/components/internal/apps/apps-page.component.html
  17. 6
      src/Squidex/app/components/internal/apps/apps-page.component.scss
  18. 4
      src/Squidex/app/components/internal/internal-area.component.html
  19. 12
      src/Squidex/app/components/internal/internal-area.component.scss
  20. 10
      src/Squidex/app/components/layout/apps-menu-list.component.scss
  21. 4
      src/Squidex/app/components/layout/apps-menu.component.html
  22. 50
      src/Squidex/app/components/layout/apps-menu.component.scss
  23. 1
      src/Squidex/app/components/layout/declarations.ts
  24. 3
      src/Squidex/app/components/layout/module.ts
  25. 13
      src/Squidex/app/components/layout/profile-menu.component.html
  26. 39
      src/Squidex/app/components/layout/profile-menu.component.scss
  27. 34
      src/Squidex/app/components/layout/profile-menu.component.ts
  28. 14
      src/Squidex/app/components/layout/search-form.component.scss
  29. 1
      src/Squidex/app/components/public/declarations.ts
  30. 2
      src/Squidex/app/components/public/home-page.component.html
  31. 30
      src/Squidex/app/components/public/home-page.component.ts
  32. 6
      src/Squidex/app/components/public/module.ts
  33. 59
      src/Squidex/app/framework/angular/color-picker.component.scss
  34. 46
      src/Squidex/app/framework/angular/modal-view.directive.ts
  35. 34
      src/Squidex/app/framework/angular/slider.component.scss
  36. 9
      src/Squidex/app/shared/guards/must-be-authenticated.guard.ts
  37. 31
      src/Squidex/app/shared/guards/must-be-not-authenticated.guard.ts
  38. 3
      src/Squidex/app/shared/index.ts
  39. 8
      src/Squidex/app/shared/services/apps-store.service.ts
  40. 50
      src/Squidex/app/shared/services/auth.service.ts
  41. 53
      src/Squidex/app/theme/_bootstrap.scss
  42. 4
      src/Squidex/app/theme/_layout.scss
  43. 40
      src/Squidex/app/theme/_mixins.scss
  44. 23
      src/Squidex/app/theme/_vars.scss
  45. 6
      src/Squidex/app/theme/_vendor-overrides.scss
  46. 1
      src/Squidex/package.json
  47. BIN
      src/Squidex/wwwroot/images/logo.png

17
src/Squidex.Infrastructure/Security/ExtendedClaimTypes.cs

@ -0,0 +1,17 @@
// ==========================================================================
// ExtendedClaimTypes.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.Security
{
public class ExtendedClaimTypes
{
public const string SquidexDisplayName = "urn:squidex:name";
public const string SquidexPictureUrl = "urn:squidex:picture";
}
}

19
src/Squidex/.sass-lint.yml

@ -0,0 +1,19 @@
rules:
no-ids:
- 0
final-newline:
- 0
clean-import-paths:
- 0
property-sort-order:
- 0
indentation:
- 2
-
size: 4
leading-underscore: false
files:
ignore:
- 'app/theme/_mixins.scss'

2
src/Squidex/Configurations/Constants.cs

@ -13,6 +13,8 @@ namespace Squidex.Configurations
public const string ApiScope = "squidex-api";
public const string ProfileScope = "squidex-profile";
public const string FrontendClient = "squidex-frontend";
public const string IdentityPrefix = "/identity-server";

10
src/Squidex/Configurations/Identity/IdentityServices.cs

@ -15,6 +15,7 @@ using IdentityServer4.Stores.InMemory;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity.MongoDB;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure.Security;
namespace Squidex.Configurations.Identity
{
@ -70,6 +71,15 @@ namespace Squidex.Configurations.Identity
StandardScopes.OpenId,
StandardScopes.Profile,
new Scope
{
Name = Constants.ProfileScope, Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim(ExtendedClaimTypes.SquidexDisplayName, true),
new ScopeClaim(ExtendedClaimTypes.SquidexPictureUrl, true)
}
},
new Scope
{
Name = Constants.ApiScope, Type = ScopeType.Resource
}

56
src/Squidex/Configurations/Identity/IdentityUsage.cs

@ -6,13 +6,21 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.MongoDB;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure.Security;
// ReSharper disable InvertIf
@ -68,6 +76,7 @@ namespace Squidex.Configurations.Identity
var googleOptions =
new GoogleOptions
{
Events = new GoogleHandler(),
ClientId = options.GoogleClient,
ClientSecret = options.GoogleSecret
};
@ -98,5 +107,52 @@ namespace Squidex.Configurations.Identity
return app;
}
private class RetrieveClaimsHandler : OAuthEvents
{
public override Task CreatingTicket(OAuthCreatingTicketContext context)
{
var displayNameClaim = context.Identity.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Name);
if (displayNameClaim != null)
{
context.Identity.AddClaim(new Claim(ExtendedClaimTypes.SquidexDisplayName, displayNameClaim.Value));
}
return base.CreatingTicket(context);
}
}
private sealed class GoogleHandler : RetrieveClaimsHandler
{
private static readonly HttpClient HttpClient = new HttpClient();
public override Task RedirectToAuthorizationEndpoint(OAuthRedirectToAuthorizationContext context)
{
context.Response.Redirect(context.RedirectUri + "&prompt=select_account");
return Task.FromResult(true);
}
public override async Task CreatingTicket(OAuthCreatingTicketContext context)
{
if (!string.IsNullOrWhiteSpace(context.AccessToken))
{
var apiRequestUri = new Uri($"https://www.googleapis.com/oauth2/v2/userinfo?access_token={context.AccessToken}");
var jsonReponseString =
await HttpClient.GetStringAsync(apiRequestUri);
var jsonResponse = JToken.Parse(jsonReponseString);
var pictureUrl = jsonResponse["picture"]?.Value<string>();
if (!string.IsNullOrWhiteSpace(pictureUrl))
{
context.Identity.AddClaim(new Claim(ExtendedClaimTypes.SquidexPictureUrl, pictureUrl));
}
}
await base.CreatingTicket(context);
}
}
}
}

10
src/Squidex/Configurations/Identity/LazyClientStore.cs

@ -47,20 +47,22 @@ namespace Squidex.Configurations.Identity
ClientName = id,
RedirectUris = new List<string>
{
options.BuildUrl("#/login;"),
options.BuildUrl("#/logout;"),
options.BuildUrl("login;"),
options.BuildUrl("logout;"),
options.BuildUrl("identity-server/client-callback-silent/"),
options.BuildUrl("identity-server/client-callback-popup/")
},
PostLogoutRedirectUris = new List<string>
{
options.BuildUrl("logout;"),
},
AllowAccessTokensViaBrowser = true,
AllowedGrantTypes = GrantTypes.Implicit,
AllowedScopes = new List<string>
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
Constants.ApiScope
Constants.ApiScope,
Constants.ProfileScope
},
RequireConsent = false
};

36
src/Squidex/Modules/UI/Account/AccountController.cs

@ -9,10 +9,12 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.MongoDB;
using Microsoft.AspNetCore.Mvc;
using Squidex.Infrastructure.Security;
// ReSharper disable RedundantIfElseBlock
// ReSharper disable ConvertIfStatementToReturnStatement
@ -23,11 +25,16 @@ namespace Squidex.Modules.UI.Account
{
private readonly SignInManager<IdentityUser> signInManager;
private readonly UserManager<IdentityUser> userManager;
private readonly IIdentityServerInteractionService interactions;
public AccountController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
public AccountController(
SignInManager<IdentityUser> signInManager,
UserManager<IdentityUser> userManager,
IIdentityServerInteractionService interactions)
{
this.signInManager = signInManager;
this.userManager = userManager;
this.interactions = interactions;
}
[Authorize]
@ -59,6 +66,17 @@ namespace Squidex.Modules.UI.Account
return View();
}
[HttpGet]
[Route("account/logout/")]
public async Task<IActionResult> Logout(string logoutId)
{
var context = await interactions.GetLogoutContextAsync(logoutId);
await signInManager.SignOutAsync();
return context.PostLogoutRedirectUri != null ? (IActionResult)Redirect(context.PostLogoutRedirectUri) : StatusCode(201);
}
[HttpGet]
[Route("account/login/")]
public IActionResult Login(string returnUrl = null)
@ -144,7 +162,21 @@ namespace Squidex.Modules.UI.Account
{
var mail = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value;
return new IdentityUser { Email = mail, UserName = mail };
var user = new IdentityUser { Email = mail, UserName = mail };
var profileUrl = externalLogin.Principal.Claims.FirstOrDefault(x => x.Type == ExtendedClaimTypes.SquidexPictureUrl);
if (profileUrl != null)
{
user.AddClaim(profileUrl);
}
var displayName = externalLogin.Principal.Claims.FirstOrDefault(x => x.Type == ExtendedClaimTypes.SquidexDisplayName);
if (displayName != null)
{
user.AddClaim(displayName);
}
return user;
}
}
}

1
src/Squidex/Properties/launchSettings.json

@ -10,7 +10,6 @@
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

10
src/Squidex/app/app.module.ts

@ -14,12 +14,13 @@ import {
ApiUrlConfig,
AppsStoreService,
AppsService,
AuthGuard,
AuthService,
CurrencyConfig,
DragService,
DragServiceFactory,
DecimalSeparatorConfig,
MustBeAuthenticatedGuard,
MustBeNotAuthenticatedGuard,
SqxFrameworkModule,
TitlesConfig,
TitleService
@ -27,8 +28,8 @@ import {
import {
SqxAppModule,
SqxAuthModule,
SqxLayoutModule,
SqxLoginModule,
SqxPublicModule
} from './components';
@ -40,8 +41,8 @@ const baseUrl = window.location.protocol + '//' + window.location.host + '/';
imports: [
Ng2Browser.BrowserModule,
SqxAppModule,
SqxAuthModule,
SqxLayoutModule,
SqxLoginModule,
SqxFrameworkModule,
SqxPublicModule,
routing
@ -52,8 +53,9 @@ const baseUrl = window.location.protocol + '//' + window.location.host + '/';
providers: [
AppsStoreService,
AppsService,
AuthGuard,
AuthService,
MustBeAuthenticatedGuard,
MustBeNotAuthenticatedGuard,
TitleService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig(baseUrl) },
{ provide: CurrencyConfig, useValue: new CurrencyConfig('EUR', '€', true) },

14
src/Squidex/app/app.routes.ts

@ -13,24 +13,26 @@ import {
AppAreaComponent,
DashboardComponent,
InternalAreaComponent,
LoginPageComponent,
HomePageComponent,
LogoutPageComponent,
NotFoundPageComponent
} from './components';
import {
AuthGuard
MustBeAuthenticatedGuard,
MustBeNotAuthenticatedGuard
} from './shared';
export const routes: Ng2Router.Routes = [
{
path: '',
redirectTo: 'app', pathMatch: 'full'
component: HomePageComponent,
canActivate: [MustBeNotAuthenticatedGuard],
},
{
path: 'app',
component: InternalAreaComponent,
canActivate: [AuthGuard],
canActivate: [MustBeAuthenticatedGuard],
children: [
{
path: '',
@ -48,10 +50,6 @@ export const routes: Ng2Router.Routes = [
}
]
},
{
path: 'login',
component: LoginPageComponent
},
{
path: 'logout',
component: LogoutPageComponent

1
src/Squidex/app/components/auth/declarations.ts

@ -5,5 +5,4 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export * from './login-page.component';
export * from './logout-page.component';

5
src/Squidex/app/components/auth/login-page.component.html

@ -1,5 +0,0 @@
<div *ngIf="showError">
<div class="simple-error">
Failed to login
</div>
</div>

5
src/Squidex/app/components/auth/logout-page.component.html

@ -1,5 +0,0 @@
<div *ngIf="showError">
<div class="simple-error">
Failed to login
</div>
</div>

6
src/Squidex/app/components/auth/logout-page.component.ts

@ -12,17 +12,17 @@ import { AuthService } from 'shared';
@Ng2.Component({
selector: 'logout',
template
template: ''
})
export class LogoutPageComponent implements Ng2.OnInit {
constructor(
private readonly authService: AuthService,
private readonly auth: AuthService,
private readonly router: Ng2Router.Router
) {
}
public ngOnInit() {
this.authService.logoutComplete().subscribe(
this.auth.logoutComplete().subscribe(
() => {
this.router.navigate(['/'], { replaceUrl: true });
},

4
src/Squidex/app/components/auth/module.ts

@ -10,7 +10,6 @@ import * as Ng2 from '@angular/core';
import { SqxFrameworkModule } from 'shared';
import {
LoginPageComponent,
LogoutPageComponent
} from './declarations';
@ -19,8 +18,7 @@ import {
SqxFrameworkModule
],
declarations: [
LoginPageComponent,
LogoutPageComponent
]
})
export class SqxLoginModule { }
export class SqxAuthModule { }

2
src/Squidex/app/components/internal/apps/apps-page.component.html

@ -6,7 +6,7 @@
</div>
</content>
<div class="modal" [(sqxModalView)]="modalDialog" [@fade]="modalDialog.isOpen | async">
<div class="modal" *sqxModalView="modalDialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">

6
src/Squidex/app/components/internal/apps/apps-page.component.scss

@ -1,5 +1,5 @@
@import '_vars.scss';
@import '_mixins.scss';
@import '_vars';
@import '_mixins';
content {
padding: 20px;
@ -11,7 +11,7 @@ content {
}
&-headline {
margin-top: 100px;
margin-bottom: 20px;
margin-top: 100px;
}
}

4
src/Squidex/app/components/internal/internal-area.component.html

@ -10,6 +10,10 @@
<div class="float-xs-left search-form">
<sqx-search-form></sqx-search-form>
</div>
<div class="float-xs-right profile-menu">
<sqx-profile-menu></sqx-profile-menu>
</div>
</nav>
<main>

12
src/Squidex/app/components/internal/internal-area.component.scss

@ -1,15 +1,15 @@
@import '_vars.scss';
@import '_mixins.scss';
@import '_vars';
@import '_mixins';
.navbar {
@include box-shadow(0, 4px, 4px, 0.2px);
@include box-shadow(0, 4px, 4px, .2px);
}
.navbar-brand {
padding-top: 0;
padding-bottom: 0;
margin-top: -0.2rem;
margin-bottom: -0.2rem;
margin-top: -.2rem;
margin-bottom: -.2rem;
font-size: 1.8rem;
}
@ -18,5 +18,5 @@
}
main {
margin-top: 54px
margin-top: 54px;
}

10
src/Squidex/app/components/layout/apps-menu-list.component.scss

@ -1,5 +1,5 @@
@import '_vars.scss';
@import '_mixins.scss';
@import '_vars';
@import '_mixins';
.all-apps {
& {
@ -12,8 +12,8 @@
&-pill {
@include absolute(6px, 10px, auto, auto);
color: $accent-blue;
border: none;
background: $accent-blue-lighter;
background: $theme-blue-lighter;
border: 0;
color: $theme-blue;
}
}

4
src/Squidex/app/components/layout/apps-menu.component.html

@ -2,7 +2,7 @@
<li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" id="app-name" (click)="modalMenu.toggle()">{{app | async}}</span>
<div class="dropdown-menu" [(sqxModalView)]="modalMenu" [@fade]="(modalMenu.isOpen | async)">
<div class="dropdown-menu" *sqxModalView="modalMenu">
<sqx-apps-menu-list [apps]="apps | async"></sqx-apps-menu-list>
<div class="drodown-button">
@ -12,7 +12,7 @@
</li>
</ul>
<div class="modal ng-animate" [(sqxModalView)]="modalDialog" [@fade]="(modalDialog.isOpen | async)">
<div class="modal ng-animate" *sqxModalView="modalDialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">

50
src/Squidex/app/components/layout/apps-menu.component.scss

@ -1,12 +1,16 @@
@import '_vars.scss';
@import '_mixins.scss';
.navbar-dark .navbar-nav .nav-link {
color: white;
@import '_vars';
@import '_mixins';
.navbar-dark {
.navbar-nav {
.nav-link {
color: $accent-dark;
}
}
}
.nav-link {
@include truncate();
@include truncate;
}
.drodown-button {
@ -14,26 +18,28 @@
}
.dropdown-menu {
top: 44px;
}
& {
top: 44px;
}
.dropdown-menu:before {
@include absolute(-18px, auto, auto, 10px);
content: '';
height: 0;
border-style: solid;
border-width: 10px;
border-color: transparent transparent white transparent;
width: 0;
&::before {
@include absolute(-18px, auto, auto, 10px);
border-color: transparent transparent $accent-dark;
border-style: solid;
border-width: 10px;
content: '';
height: 0;
width: 0;
}
}
#app-name {
& {
@include transition(opacity .4 ease);
@include opacity(.95);
padding-right: 15px;
@include transition(opacity 0.4 ease);
@include opacity(0.95);
color: white;
cursor: pointer;
color: $accent-dark;
width: 200px;
}
@ -41,8 +47,8 @@
@include opacity(1);
}
&:after {
@include absolute(50%, 0px, auto, auto);
color: $accent-blue-light;
&::after {
@include absolute(50%, 0, auto, auto);
color: $theme-blue-light;
}
}

1
src/Squidex/app/components/layout/declarations.ts

@ -8,4 +8,5 @@
export * from './app-form.component';
export * from './apps-menu.component';
export * from './apps-menu-list.component';
export * from './profile-menu.component';
export * from './search-form.component';

3
src/Squidex/app/components/layout/module.ts

@ -13,6 +13,7 @@ import {
AppFormComponent,
AppsMenuComponent,
AppsMenuListComponent,
ProfileMenuComponent,
SearchFormComponent
} from './declarations';
@ -24,12 +25,14 @@ import {
AppFormComponent,
AppsMenuComponent,
AppsMenuListComponent,
ProfileMenuComponent,
SearchFormComponent,
],
exports: [
AppFormComponent,
AppsMenuComponent,
AppsMenuListComponent,
ProfileMenuComponent,
SearchFormComponent,
]
})

13
src/Squidex/app/components/layout/profile-menu.component.html

@ -0,0 +1,13 @@
<ul class="nav navbar-nav">
<li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" (click)="modalMenu.toggle()">
<img [attr.src]="(pictureUrl | async)" />
<span>{{displayName | async}}</span>
</span>
<div class="dropdown-menu" *sqxModalView="modalMenu">
<a class="dropdown-item" (click)="logout()">Logout</a>
</div>
</li>
</ul>

39
src/Squidex/app/components/layout/profile-menu.component.scss

@ -0,0 +1,39 @@
@import '_mixins';
@import '_vars';
$size: 2.2rem;
a {
cursor: pointer;
}
img {
@include border-radius($size * .5);
height: $size;
width: $size;
}
.navbar-nav {
.nav-link {
padding: 0;
cursor: pointer;
color: $accent-dark;
line-height: 2.2rem;
}
}
.dropdown-menu {
& {
@include absolute(44px, 0, auto, auto);
}
&::before {
@include absolute(-18px, 10px, auto, auto);
border-color: transparent transparent $accent-dark;
border-style: solid;
border-width: 10px;
content: '';
height: 0;
width: 0;
}
}

34
src/Squidex/app/components/layout/profile-menu.component.ts

@ -0,0 +1,34 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
import { AuthService, ModalView } from 'shared';
@Ng2.Component({
selector: 'sqx-profile-menu',
styles,
template
})
export class ProfileMenuComponent {
public modalMenu = new ModalView();
public displayName
= this.auth.isAuthenticatedChanges.map(t => t ? this.auth.user.displayName : null);
public pictureUrl
= this.auth.isAuthenticatedChanges.map(t => t ? this.auth.user.pictureUrl : null);
constructor(
private readonly auth: AuthService
) {
}
public logout() {
this.auth.logout();
}
}

14
src/Squidex/app/components/layout/search-form.component.scss

@ -1,16 +1,16 @@
@import '_vars.scss';
@import '_mixins.scss';
@import '_vars';
@import '_mixins';
.search {
& {
@include transition(background 0.4s ease);
color: white;
background: $accent-blue-dark;
border-color: $accent-blue-dark;
@include transition(background .4s ease);
background: $theme-blue-dark;
border-color: $theme-blue-dark;
border-width: 1px;
color: $accent-dark;
}
&:focus {
background: darken($accent-blue-dark, 5%);
background: darken($theme-blue-dark, 5%);
}
}

1
src/Squidex/app/components/public/declarations.ts

@ -5,4 +5,5 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export * from './home-page.component';
export * from './not-found-page.component';

2
src/Squidex/app/components/public/home-page.component.html

@ -0,0 +1,2 @@
<button class="btn btn-primary" (click)="login()">Login</button>

30
src/Squidex/app/components/auth/login-page.component.ts → src/Squidex/app/components/public/home-page.component.ts

@ -11,29 +11,31 @@ import * as Ng2Router from '@angular/router';
import { AuthService, TitleService } from 'shared';
@Ng2.Component({
selector: 'login',
selector: 'not-found',
template
})
export class LoginPageComponent implements Ng2.OnInit {
public showError = false;
export class HomePageComponent implements Ng2.OnInit {
public showLoginError = false;
constructor(
private readonly authService: AuthService,
private readonly router: Ng2Router.Router,
private readonly auth: AuthService,
private readonly title: TitleService,
private readonly router: Ng2Router.Router,
) {
}
public ngOnInit() {
this.authService.loginComplete().subscribe(
() => {
this.router.navigate(['/'], { replaceUrl: true });
},
e => {
this.title.setTitle('Home');
}
public login() {
this.auth.loginPopup()
.subscribe(() => {
this.router.navigate(['/app']);
}, ex => {
this.title.setTitle('Login failed');
this.showError = true;
}
);
this.showLoginError = true;
});
}
}

6
src/Squidex/app/components/public/module.ts

@ -10,7 +10,8 @@ import * as Ng2 from '@angular/core';
import { SqxFrameworkModule } from 'shared';
import {
NotFoundPageComponent,
HomePageComponent,
NotFoundPageComponent
} from './declarations';
@Ng2.NgModule({
@ -18,7 +19,8 @@ import {
SqxFrameworkModule
],
declarations: [
NotFoundPageComponent
NotFoundPageComponent,
HomePageComponent
]
})
export class SqxPublicModule { }

59
src/Squidex/app/framework/angular/color-picker.component.scss

@ -1,41 +1,48 @@
@import '../../theme/_mixins.scss';
@import '_mixins';
$color-size: 24px;
$color-button: #1875cc;
$color-button-hover: #1460a8;
$color-border: #fff;
$button-size: 1.81rem;
.color-palette {
& {
@include clearfix();
width: 8 * ($color-size + 6);
@include clearfix;
padding: 6px;
width: 8 * ($color-size + 6);
}
&-box {
margin: 2px;
border: 2px solid transparent;
width: $color-size;
height: $color-size;
float: left;
}
& {
border: 2px solid transparent;
margin: 2px;
height: $color-size;
width: $color-size;
float: left;
}
&-box:hover {
border-color: #1460A8;
}
&:hover {
border-color: $color-button-hover;
}
&-box.selected {
border-color: #1875CC;
}
&.selected {
& {
border-color: $color-button;
}
&-box.selected:hover {
border-color: #1875CC;
}
&:hover {
border-color: $color-button;
}
}
&-box.disabled {
border-color: transparent;
&.disabled {
border-color: transparent;
}
}
&-color {
border: 1px solid white;
border: 1px solid $color-border;
width: $color-size - 4;
height: $color-size - 4;
display: block;
@ -43,11 +50,15 @@ $button-size: 1.81rem;
}
.dropdown-menu {
background: #eee;
background: $color-border;
}
.btn-group > .btn:first-child {
@include border-radius(0.25em);
.btn-group {
> .btn {
&:first-child {
@include border-radius(.25em);
}
}
}
.btn {

46
src/Squidex/app/framework/angular/modal-view.directive.ts

@ -12,29 +12,45 @@ import { ModalView } from './../utils/modal-view';
@Ng2.Directive({
selector: '[sqxModalView]'
})
export class ModalViewDirective implements Ng2.OnChanges {
export class ModalViewDirective implements Ng2.OnChanges, Ng2.OnInit, Ng2.OnDestroy {
private subscription: any | null;
private isEnabled = true;
private clickHandler: Function | null;
private view: Ng2.EmbeddedViewRef<any> | null;
@Ng2.Input('sqxModalView')
public modalView: ModalView;
constructor(
private readonly elementRef: Ng2.ElementRef,
private readonly templateRef: Ng2.TemplateRef<any>,
private readonly renderer: Ng2.Renderer,
private readonly viewContainer: Ng2.ViewContainerRef
) {
}
@Ng2.HostListener('document:click', ['$event', '$event.target'])
public clickOutside(event: MouseEvent, targetElement: HTMLElement) {
if (!targetElement) {
return;
}
public ngOnInit() {
this.clickHandler =
this.renderer.listenGlobal('document', 'click', (event: MouseEvent) => {
if (!event.target || this.view === null) {
return;
}
if (this.view.rootNodes.length === 0) {
return;
}
const clickedInside = this.view.rootNodes[0].contains(event.target);
if (!clickedInside && this.modalView && this.isEnabled) {
this.modalView.hide();
}
});
}
const clickedInside = this.elementRef.nativeElement.contains(targetElement);
if (!clickedInside && this.modalView && this.isEnabled) {
this.modalView.hide();
public ngOnDestroy() {
if (this.clickHandler) {
this.clickHandler();
this.clickHandler = null;
}
}
@ -48,9 +64,13 @@ export class ModalViewDirective implements Ng2.OnChanges {
this.subscription = this.modalView.isOpen.subscribe(isOpen => {
if (this.isEnabled) {
if (isOpen) {
this.renderer.setElementStyle(this.elementRef.nativeElement, 'display', 'block');
this.view = this.viewContainer.createEmbeddedView(this.templateRef);
this.renderer.setElementStyle(this.view.rootNodes[0], 'display', 'block');
} else {
this.renderer.setElementStyle(this.elementRef.nativeElement, 'display', 'none');
this.view = null;
this.viewContainer.clear();
}
this.updateEnabled();

34
src/Squidex/app/framework/angular/slider.component.scss

@ -1,37 +1,39 @@
@import '../../theme/_mixins.scss';
@import '_mixins';
$bar-height: 12px;
$thumb-size: 20px;
$thumb-margin: ($thumb-size - $bar-height) * 0.5;
$thumb-margin: ($thumb-size - $bar-height) * .5;
$color-border: #ccc;
$color-focus: #66afe9;
.slider {
&-bar {
@include border-radius($bar-height * 0.5);
@include border-radius($bar-height * .5);
position: relative;
border: 1px solid $color-border;
margin-bottom: 20px;
margin-top: 5px;
margin-right: $thumb-size * 0.5;
background: white;
margin-right: $thumb-size * .5;
background: $color-border;
height: $bar-height;
}
&-thumb {
@include border-radius($thumb-size * 0.5);
position: absolute;
width: $thumb-size;
height: $thumb-size;
border: 1px solid $color-border;
background: white;
margin-top: -$thumb-margin;
margin-left: -$thumb-size * 0.5;
}
& {
@include border-radius($thumb-size * .5);
position: absolute;
width: $thumb-size;
height: $thumb-size;
border: 1px solid $color-border;
background: $color-border;
margin-top: -$thumb-margin;
margin-left: -$thumb-size * .5;
}
&-thumb.focused {
border-color: $color-focus;
&.focused {
border-color: $color-focus;
}
}
}

9
src/Squidex/app/shared/guards/auth.guard.ts → src/Squidex/app/shared/guards/must-be-authenticated.guard.ts

@ -11,16 +11,17 @@ import * as Ng2Router from '@angular/router';
import { AuthService } from './../services/auth.service';
@Ng2.Injectable()
export class AuthGuard implements Ng2Router.CanActivate {
export class MustBeAuthenticatedGuard implements Ng2Router.CanActivate {
constructor(
private readonly authService: AuthService
private readonly auth: AuthService,
private readonly router: Ng2Router.Router
) {
}
public canActivate(route: Ng2Router.ActivatedRouteSnapshot, state: Ng2Router.RouterStateSnapshot): Promise<boolean> | boolean {
return this.authService.checkLogin().then(isAuthenticated => {
return this.auth.checkLogin().then(isAuthenticated => {
if (!isAuthenticated) {
this.authService.login();
this.router.navigate(['']);
return false;
}

31
src/Squidex/app/shared/guards/must-be-not-authenticated.guard.ts

@ -0,0 +1,31 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
import * as Ng2Router from '@angular/router';
import { AuthService } from './../services/auth.service';
@Ng2.Injectable()
export class MustBeNotAuthenticatedGuard implements Ng2Router.CanActivate {
constructor(
private readonly auth: AuthService,
private readonly router: Ng2Router.Router
) {
}
public canActivate(route: Ng2Router.ActivatedRouteSnapshot, state: Ng2Router.RouterStateSnapshot): Promise<boolean> | boolean {
return this.auth.checkLogin().then(isAuthenticated => {
if (isAuthenticated) {
this.router.navigate(['app']);
return false;
}
return true;
});
}
}

3
src/Squidex/app/shared/index.ts

@ -5,7 +5,8 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export * from './guards/auth.guard';
export * from './guards/must-be-authenticated.guard';
export * from './guards/must-be-not-authenticated.guard';
export * from './services/apps-store.service';
export * from './services/apps.service';
export * from './services/auth.service';

8
src/Squidex/app/shared/services/apps-store.service.ts

@ -27,10 +27,10 @@ export class AppsStoreService {
}
constructor(
private readonly authService: AuthService,
private readonly auth: AuthService,
private readonly appService: AppsService
) {
if (!authService || !appService) {
if (!auth || !appService) {
return;
}
@ -38,7 +38,7 @@ export class AppsStoreService {
this.lastApps = apps;
});
this.authService.isAuthenticatedChanges.subscribe(isAuthenticated => {
this.auth.isAuthenticatedChanges.subscribe(isAuthenticated => {
if (isAuthenticated) {
this.load();
}
@ -46,7 +46,7 @@ export class AppsStoreService {
}
public reload() {
if (this.authService.isAuthenticated) {
if (this.auth.isAuthenticated) {
this.load();
}
}

50
src/Squidex/app/shared/services/auth.service.ts

@ -19,14 +19,28 @@ import { BehaviorSubject, Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework';
export class Profile {
public get displayName() {
return this.user.profile['urn:squidex:name'];
}
public get pictureUrl() {
return this.user.profile['urn:squidex:picture'];
}
constructor(
public readonly user: User
) {
}
}
@Ng2.Injectable()
export class AuthService {
private readonly userManager: UserManager;
private readonly isAuthenticatedChanged$ = new BehaviorSubject<boolean>(false);
private currentUser: User | null = null;
private checkLoginPromise: Promise<boolean>;
private currentUser: Profile | null = null;
public get user(): User | null {
public get user(): Profile | null {
return this.currentUser;
}
@ -47,7 +61,7 @@ export class AuthService {
if (apiUrl) {
this.userManager = new UserManager({
client_id: 'squidex-frontend',
scope: 'squidex-api openid profile ',
scope: 'squidex-api openid profile squidex-profile',
response_type: 'id_token token',
redirect_uri: apiUrl.buildUrl('/login;'),
post_logout_redirect_uri: apiUrl.buildUrl('/logout;'),
@ -70,18 +84,16 @@ export class AuthService {
}
public checkLogin(): Promise<boolean> {
if (this.checkLoginPromise) {
return this.checkLoginPromise;
} else if (this.currentUser) {
if (this.currentUser) {
return Promise.resolve(true);
} else {
this.checkLoginPromise =
const promise =
this.checkState(this.userManager.signinSilent())
.then(result => {
return result || this.checkState(this.userManager.signinSilent());
});
return this.checkLoginPromise;
return promise;
}
}
@ -101,16 +113,26 @@ export class AuthService {
return Observable.fromPromise(this.userManager.signinRedirectCallback());
}
private onAuthenticated(user: User) {
this.currentUser = user;
public loginPopup(): Observable<boolean> {
const promise =
this.checkState(this.userManager.signinPopup())
.then(result => {
return result || this.checkState(this.userManager.signinSilent());
});
return Observable.fromPromise(promise);
}
private onAuthenticated(user: User) {
this.isAuthenticatedChanged$.next(true);
this.currentUser = new Profile(user);
}
private onDeauthenticated() {
this.currentUser = null;
this.isAuthenticatedChanged$.next(false);
this.currentUser = null;
}
private checkState(promise: Promise<User>): Promise<boolean> {
@ -172,7 +194,7 @@ export class AuthService {
options.headers = new Ng2Http.Headers();
options.headers.append('Content-Type', 'application/json');
}
options.headers.append('Authorization', `${this.currentUser.token_type} ${this.currentUser.access_token}`);
options.headers.append('Authorization', `${this.currentUser.user.token_type} ${this.currentUser.user.access_token}`);
return options;
}

53
src/Squidex/app/theme/_bootstrap.scss

@ -1,26 +1,31 @@
@import '_mixins.scss';
@import '_vars.scss';
@import '_mixins';
@import '_vars';
.form-hint {
font-size: 0.8rem;
font-size: .8rem;
}
.form-error {
@include border-radius(3px);
@include truncate();
color: white;
@include truncate;
color: $accent-dark;
margin-top: 4px;
margin-bottom: 10px;
padding: 6px;
background: $accent-error;
background: $theme-error;
}
.ng-invalid.ng-dirty {
border-color: $accent-error;
}
.ng-invalid {
&.ng-dirty {
& {
border-color: $theme-error;
}
.ng-invalid.ng-dirty :focus, .ng-invalid.ng-dirty :hover {
border-color: $accent-error-dark;
&:hover,
&:focus {
border-color: $theme-error-dark;
}
}
}
.errors {
@ -28,37 +33,39 @@
position: relative;
}
&:after {
@include absolute(2rem, auto, auto, 0.6rem);
&::after {
@include absolute(2rem, auto, auto, .6rem);
content: '';
height: 0;
border-style: solid;
border-width: 0.4rem;
border-color: $accent-error transparent transparent transparent;
border-width: .4rem;
border-color: $theme-error transparent transparent;
width: 0;
}
& {
@include absolute(-2.4rem, 0px, auto, 0px);
@include absolute(-2.4rem, 0, auto, 0);
@include border-radius(2px);
color: white;
color: $accent-dark;
cursor: none;
font-size: 0.9rem;
font-size: .9rem;
font-weight: normal;
line-height: 2rem;
padding: 0 0.4rem;
background: $accent-error;
padding: 0 .4rem;
background: $theme-error;
}
}
.modal-content, .dropdown-menu {
@include box-shadow(0px, 6px, 16px, 0.2px);
.modal-content {
@include box-shadow(0, 6px, 16px, .2px);
}
.modal-content, .modal-header {
.modal-content,
.modal-header {
border: 0;
}
.dropdown-menu {
@include box-shadow(0, 6px, 16px, .2px);
border: 0;
}

4
src/Squidex/app/theme/_layout.scss

@ -1,5 +1,5 @@
@import '_mixins.scss';
@import '_vars.scss';
@import '_mixins';
@import '_vars';
body {
overflow: hidden;

40
src/Squidex/app/theme/_mixins.scss

@ -1,5 +1,5 @@
@mixin clearfix() {
&:after {
&::after {
content: "";
display: table;
clear: both;
@ -7,34 +7,34 @@
}
@mixin flex-box {
display: -webkit-box;
display: -webkit-flex;
display: -moz-flex;
display: -ms-flexbox;
display: flex;
display: -webkit-box;
display: -webkit-flex;
display: -moz-flex;
display: -ms-flexbox;
display: flex;
}
@mixin flex-inline {
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -moz-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -moz-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
}
@mixin flex-flow($values: (row nowrap)) {
-webkit-flex-flow: $values;
-moz-flex-flow: $values;
-ms-flex-flow: $values;
flex-flow: $values;
-webkit-flex-flow: $values;
-moz-flex-flow: $values;
-ms-flex-flow: $values;
flex-flow: $values;
}
@mixin flex-grow($int: 0) {
-webkit-box-flex: $int;
-webkit-flex-grow: $int;
-moz-flex-grow: $int;
-ms-flex-positive: $int;
flex-grow: $int;
-webkit-box-flex: $int;
-webkit-flex-grow: $int;
-moz-flex-grow: $int;
-ms-flex-positive: $int;
flex-grow: $int;
}
@mixin inner-border($color: #999) {

23
src/Squidex/app/theme/_vars.scss

@ -1,15 +1,18 @@
$nav-text-color: #333;
$background: #F4F8F9;
$border: #EAEEEF;
$background: #f4f8f9;
$border: #eaeeef;
$accent-blue: #438CEF;
$accent-blue-dark: #3F83DF;
$accent-blue-light: #A1C6F7;
$accent-blue-lighter: #D9E8FC;
$theme-blue: #438cef;
$theme-blue-dark: #3f83df;
$theme-blue-light: #a1c6f7;
$theme-blue-lighter: #d9e8fc;
$accent-green: #4CC159;
$accent-green-dark: #47B353;
$theme-green: #4cc159;
$theme-green-dark: #47b353;
$theme-error: #f00;
$theme-error-dark: darken($theme-error, 5%);
$accent-dark: #fff;
$accent-error: red;
$accent-error-dark: darken(red, 5%);

6
src/Squidex/app/theme/_vendor-overrides.scss

@ -1,7 +1,7 @@
@import '_mixins.scss';
@import '_vars.scss';
$fa-font-path: "~font-awesome/fonts";
$fa-font-path: '~font-awesome/fonts';
$brand-primary: $accent-blue;
$brand-success: $accent-green;
$brand-primary: $theme-blue;
$brand-success: $theme-green;

1
src/Squidex/package.json

@ -65,6 +65,7 @@
"phantomjs-prebuilt": "^2.1.7",
"raw-loader": "^0.5.1",
"rimraf": "^2.5.3",
"sass-lint": "^1.9.1",
"sass-loader": "^4.0.0",
"style-loader": "^0.13.1",
"tslint": "^3.13.0-dev.0",

BIN
src/Squidex/wwwroot/images/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Loading…
Cancel
Save