Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Implement magic login #8387

Merged
merged 14 commits into from
Oct 13, 2024
Merged
5 changes: 4 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -1088,7 +1088,10 @@
"tsConfig": "apps/desktop-timer/tsconfig.app.json",
"aot": true,
"stylePreprocessorOptions": {
"includePaths": ["apps/desktop-timer/src/assets/styles"]
"includePaths": [
"apps/desktop-timer/src/assets/styles",
"packages/ui-core/static/styles"
]
},
"assets": [
"apps/desktop-timer/src/favicon.ico",
Expand Down
28 changes: 2 additions & 26 deletions apps/desktop-timer/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import {
AlwaysOnComponent,
AuthGuard,
ImageViewerComponent,
NgxLoginComponent,
NoAuthGuard,
ScreenCaptureComponent,
ServerDownPage,
SettingsComponent,
Expand All @@ -15,7 +13,6 @@ import {
TimeTrackerComponent,
UpdaterComponent
} from '@gauzy/desktop-ui-lib';
import { NbAuthComponent, NbRequestPasswordComponent, NbResetPasswordComponent } from '@nebular/auth';
import { AppModuleGuard } from './app.module.guards';

const routes: Routes = [
Expand All @@ -25,29 +22,8 @@ const routes: Routes = [
},
{
path: 'auth',
component: NbAuthComponent,
children: [
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
},
{
path: 'login',
component: NgxLoginComponent,
canActivate: [AppModuleGuard, NoAuthGuard]
},
{
path: 'request-password',
component: NbRequestPasswordComponent,
canActivate: [AppModuleGuard, NoAuthGuard]
},
{
path: 'reset-password',
component: NbResetPasswordComponent,
canActivate: [AppModuleGuard, NoAuthGuard]
}
]
loadChildren: () => import('@gauzy/desktop-ui-lib').then((m) => m.authRoutes),
canActivate: [AppModuleGuard]
},
{
path: 'time-tracker',
Expand Down
75 changes: 75 additions & 0 deletions packages/desktop-ui-lib/src/lib/auth/auth.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Route } from '@angular/router';
import {
NbAuthComponent,
NbLogoutComponent,
NbRegisterComponent,
NbRequestPasswordComponent,
NbResetPasswordComponent
} from '@nebular/auth';
import { NgxLoginComponent } from '../login';
import { NgxLoginMagicComponent } from '../login/features/login-magic/login-magic.component';
import { NgxLoginWorkspaceComponent } from '../login/features/login-workspace/login-workspace.component';
import { NgxMagicSignInWorkspaceComponent } from '../login/features/magic-login-workspace/magic-login-workspace.component';
import { NoAuthGuard } from './no-auth.guard';

export const authRoutes: Route[] = [
{
path: '',
component: NbAuthComponent,
children: [
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
},
{
path: 'login',
component: NgxLoginComponent,
canActivate: [NoAuthGuard]
},
{
path: 'register',
component: NbRegisterComponent,
canActivate: [NoAuthGuard]
},
{
path: 'logout',
component: NbLogoutComponent
},
{
path: 'request-password',
component: NbRequestPasswordComponent,
canActivate: [NoAuthGuard]
},
{
path: 'reset-password',
component: NbResetPasswordComponent,
canActivate: [NoAuthGuard]
},
{
// Register the path 'login-workspace'
path: 'login-workspace',
// Register the component to load component: NgxLoginWorkspaceComponent,
component: NgxLoginWorkspaceComponent,
// Register the data object
canActivate: [NoAuthGuard]
},
{
// Register the path 'login-magic'
path: 'login-magic',
// Register the component to load component: NgxLoginMagicComponent,
component: NgxLoginMagicComponent,
// Register the data object
canActivate: [NoAuthGuard]
},
{
// Register the path 'magic-sign-in'
path: 'magic-sign-in',
// Register the component to load component: NgxMagicSignInWorkspaceComponent,
component: NgxMagicSignInWorkspaceComponent,
// Register the data object
canActivate: [NoAuthGuard]
}
]
}
];
4 changes: 2 additions & 2 deletions packages/desktop-ui-lib/src/lib/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './services';
export * from './auth.guard';
export * from './no-auth.guard';
export * from './auth.module';
export * from './auth.routes';
export * from './no-auth.guard';
export * from './services';
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Directive, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Subject, Subscription, tap } from 'rxjs';
import { debounceTime } from 'rxjs/operators';

@Directive({
selector: '[debounceClick]'
})
export class DebounceClickDirective implements OnInit, OnDestroy {
adkif marked this conversation as resolved.
Show resolved Hide resolved
private clicks: Subject<Event> = new Subject<Event>();
private subscription: Subscription;

@Input() debounceTime = 300;
@Output() throttledClick: EventEmitter<Event> = new EventEmitter<Event>();

/**
* Handles the click event and emits it after a debounce time.
*
* @param {Event} event - The click event object.
* @return {void} This function does not return a value.
*/
@HostListener('click', ['$event'])
clickEvent(event: Event): void {
this.clicks.next(event);
}

ngOnInit() {
this.subscription = this.clicks
.pipe(
debounceTime(this.debounceTime),
tap((e) => this.throttledClick.emit(e))
)
.subscribe();
}

ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { NbSpinnerModule } from '@nebular/theme';
import { DebounceClickDirective } from './debounce-click.directive';
import { DynamicDirective } from './dynamic.directive';
import { SpinnerButtonDirective } from './spinner-button.directive';
import { TextMaskDirective } from './text-mask.directive';

@NgModule({
declarations: [SpinnerButtonDirective, DynamicDirective, TextMaskDirective],
exports: [SpinnerButtonDirective, DynamicDirective, TextMaskDirective],
declarations: [SpinnerButtonDirective, DynamicDirective, TextMaskDirective, DebounceClickDirective],
exports: [SpinnerButtonDirective, DynamicDirective, TextMaskDirective, DebounceClickDirective],
imports: [CommonModule, NbSpinnerModule]
})
export class DesktopDirectiveModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<section class="login-container">
<div class="login-wrapper">
<div class="svg-wrapper">
<gauzy-logo></gauzy-logo>
<gauzy-switch-theme></gauzy-switch-theme>
</div>
<div class="headings" [class]="isDemo ? 'headings-demo' : ''">
<div class="headings-inner">
<h2 id="title" class="title">{{ 'LOGIN_PAGE.TITLE' | translate }}</h2>
<p class="sub-title">{{ 'LOGIN_PAGE.LOGIN_MAGIC.TITLE' | translate }}</p>
</div>
<ng-template [ngIf]="isCodeSent">
<div class="sent-code-container">
<p
[ngClass]="{
'normal-text': email?.value.length < 30,
'minimum-text': email?.value.length >= 30
}"
>
{{ 'LOGIN_PAGE.LOGIN_MAGIC.SUCCESS_SENT_CODE_TITLE' | translate }}
<b class="title">{{ email?.value }}</b>
<br />
<span>{{ 'LOGIN_PAGE.LOGIN_MAGIC.SUCCESS_SENT_CODE_SUB_TITLE' | translate }}</span>
</p>
</div>
</ng-template>
</div>
<div class="hr-div-strong"></div>
<!-- -->
<form #formDirective="ngForm" [formGroup]="form" (ngSubmit)="confirmSignInCode()">
<!-- Email input -->
<div class="form-control-group">
<label class="label" for="input-email">{{ 'LOGIN_PAGE.LABELS.EMAIL' | translate }}</label>
<nb-form-field>
<input
name="input-email"
id="input-email"
nbInput
fullWidth
formControlName="email"
[placeholder]="'LOGIN_PAGE.PLACEHOLDERS.EMAIL' | translate"
[status]="email.dirty ? (email.invalid ? 'danger' : 'success') : 'basic'"
[attr.aria-invalid]="email.invalid && email.touched ? true : null"
autofocus
autocomplete="off"
[ngClass]="isCodeSent ? 'not-allowed' : ''"
/>
<nb-icon
class="edit-email"
*ngIf="isCodeSent"
nbSuffix
nbButton
size="small"
ghost
icon="edit-outline"
nbTooltip="edit email"
nbTooltipPosition="top"
></nb-icon>
</nb-form-field>
<ng-container *ngIf="email.invalid && email.touched && !email.pristine">
<p class="caption status-danger" *ngIf="email?.errors?.required">
{{ 'LOGIN_PAGE.VALIDATION.EMAIL_REQUIRED' | translate }}
</p>
<p class="caption status-danger" *ngIf="email?.errors?.pattern">
{{ 'LOGIN_PAGE.VALIDATION.EMAIL_REAL_REQUIRED' | translate }}
</p>
</ng-container>
</div>
<!-- Code input -->
<ng-container *ngIf="isCodeSent">
<div class="form-control-group">
<label class="label" for="input-code">{{ 'LOGIN_PAGE.LABELS.CODE' | translate }}</label>
<input
name="input-code"
id="input-code"
nbInput
fullWidth
formControlName="code"
noSpaceEdges
[placeholder]="'LOGIN_PAGE.PLACEHOLDERS.CODE' | translate"
[status]="code.dirty ? (code.invalid ? 'danger' : 'success') : 'basic'"
[attr.aria-invalid]="code.invalid && code.touched ? true : null"
maxlength="6"
autofocus
autocomplete="off"
/>
<ng-container *ngIf="code.invalid && code.touched">
<p class="caption status-danger" *ngIf="code.errors?.required">
{{ 'LOGIN_PAGE.VALIDATION.CODE_REQUIRED' | translate }}
</p>
<p class="caption status-danger" *ngIf="code.errors?.minlength">
{{
'LOGIN_PAGE.VALIDATION.CODE_REQUIRED_LENGTH'
| translate : { requiredLength: code.errors?.minlength?.requiredLength }
}}
</p>
</ng-container>
<!-- Resend Code Button & Text -->
<ng-template [ngIf]="isCodeSent">
<p class="new-code-wrapper">
<ng-template [ngIf]="isCodeResent" [ngIfElse]="resendButton">
<span class="request-new-code">
{{
'LOGIN_PAGE.LOGIN_MAGIC.REQUEST_NEW_CODE_TITLE'
| translate : { countdown: countdown }
}}
</span>
</ng-template>

<ng-template #resendButton>
<a class="resend-code" debounceClick (throttledClick)="onResendCode()">
{{ 'LOGIN_PAGE.LOGIN_MAGIC.RESEND_CODE_TITLE' | translate }}
</a>
</ng-template>
</p>
</ng-template>
</div>
</ng-container>
<!-- Submit Button -->
<div class="submit-btn-wrapper">
<a class="forgot-email caption-2 forgot-email-big" href="mailto:forgot@gauzy.co">
{{ 'LOGIN_PAGE.FORGOT_EMAIL_TITLE' | translate }}
</a>
<div class="submit-inner-wrapper">
<ng-template [ngIf]="isCodeSent" [ngIfElse]="sendCodeButtonTemplate">
<button
type="submit"
nbButton
size="tiny"
class="submit-btn"
[disabled]="form.invalid || isLoading"
>
<span class="btn-text">
{{ 'BUTTONS.LOGIN' | translate }}
</span>
<ng-template [ngIf]="isLoading">
<nb-icon [ngStyle]="{ display: 'none' }" *gauzySpinnerButton="isLoading"></nb-icon>
</ng-template>
</button>
</ng-template>
<ng-template #sendCodeButtonTemplate>
<button
type="button"
nbButton
size="tiny"
class="submit-btn"
[disabled]="email.invalid || isLoading"
(click)="sendLoginCode()"
>
<span class="btn-text">
{{ 'BUTTONS.SEND_CODE' | translate }}
</span>
<ng-template [ngIf]="isLoading">
<nb-icon [ngStyle]="{ display: 'none' }" *gauzySpinnerButton="isLoading"></nb-icon>
</ng-template>
</button>
</ng-template>
</div>
</div>
<div class="magic-description">
<p class="sub-title">
{{ 'LOGIN_PAGE.LOGIN_MAGIC.DESCRIPTION_TITLE' | translate }}
<span class="sub-title">
<a routerLink="/auth/login">
{{ 'LOGIN_PAGE.LOGIN_MAGIC.OR_SIGN_IN_WITH_PASSWORD' | translate }}
</a>
</span>
</p>
</div>
</form>
<div class="hr-div-soft"></div>
<section>
<ngx-social-links></ngx-social-links>
</section>
<div class="hr-div-soft"></div>
<section class="another-action" aria-label="Register">
{{ 'LOGIN_PAGE.DO_NOT_HAVE_ACCOUNT_TITLE' | translate }}
<a class="text-link" routerLink="/auth/register">
{{ 'BUTTONS.REGISTER' | translate }}
</a>
</section>
</div>
</section>
Loading
Loading