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,184 @@
<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"
noSpaceEdges
[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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

noSpaceEdges Directive Confirmed

The noSpaceEdges directive is confirmed as a custom directive defined in packages/ui-core/shared/src/lib/directives/no-space-edges.directive.ts. Please ensure that its functionality and usage are properly documented to maintain codebase clarity.

🔗 Analysis chain

Email input implementation looks good, but needs clarification on some features.

The email input section is well-implemented with proper validation and error messages. However, there are a couple of points that need clarification:

  1. The purpose of the edit icon (lines 49-59) when the code is sent is not immediately clear. What action does it trigger?
  2. The noSpaceEdges directive is used on the input (line 41), but its functionality is not evident. Is this a custom directive? If so, please ensure it's documented.

Could you provide more information about the noSpaceEdges directive? If it's a custom directive, please ensure it's properly documented.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for the noSpaceEdges directive definition
rg --type typescript 'noSpaceEdges'

Length of output: 70


Script:

#!/bin/bash
# Search for the noSpaceEdges directive definition across .ts and .tsx files
rg --glob '*.ts' --glob '*.tsx' 'noSpaceEdges'

Length of output: 145

<!-- 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>
adkif marked this conversation as resolved.
Show resolved Hide resolved
<!-- 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