Skip to content

Commit c1b07cc

Browse files
committed
Add password login
1 parent 019a0e5 commit c1b07cc

15 files changed

Lines changed: 721 additions & 148 deletions
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
@if (control && control.errors && control.touched) {
2+
@for (error of errors; track error) {
3+
<div>
4+
@if (error.name === ErrorName.Required) {
5+
This field is required
6+
}
7+
@if (error.name === ErrorName.Email) {
8+
This field must be a valid email
9+
}
10+
@if (error.name === ErrorName.Min) {
11+
This field must be at least {{ error.min }}
12+
}
13+
@if (error.name === ErrorName.Max) {
14+
This field must be at most {{ error.max }}
15+
}
16+
@if (error.name === ErrorName.MinLength) {
17+
This field must be at least {{ error.requiredLength }} characters
18+
}
19+
@if (error.name === ErrorName.MaxLength) {
20+
This field must be at most {{ error.requiredLength }} characters
21+
}
22+
@if (error.name === ErrorName.Visible) {
23+
This field must not contain invisible characters or newlines
24+
}
25+
@if (error.name === ErrorName.VisibleBlock) {
26+
This field must not contain invisible characters
27+
}
28+
@if (error.name === ErrorName.KebabCase) {
29+
This field must be a valid kebab-case string (e.g. "my-worker")
30+
}
31+
@if (error.name === ErrorName.NameExists) {
32+
Name already taken
33+
}
34+
</div>
35+
}
36+
}

src/app/modules/form-error/form-error.component.ts

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -40,45 +40,7 @@ type Error = MaxError | MinError | LengthError | GenericError;
4040
@Component({
4141
imports: [],
4242
selector: 'form-error',
43-
template: `
44-
@if (control && control.errors && control.touched) {
45-
@for (error of errors; track error) {
46-
<div>
47-
@if (error.name === ErrorName.Required) {
48-
This field is required
49-
}
50-
@if (error.name === ErrorName.Email) {
51-
This field must be a valid email
52-
}
53-
@if (error.name === ErrorName.Min) {
54-
This field must be at least {{ error.min }}
55-
}
56-
@if (error.name === ErrorName.Max) {
57-
This field must be at most {{ error.max }}
58-
}
59-
@if (error.name === ErrorName.MinLength) {
60-
This field must be at least {{ error.requiredLength }} characters
61-
}
62-
@if (error.name === ErrorName.MaxLength) {
63-
This field must be at most {{ error.requiredLength }} characters
64-
}
65-
@if (error.name === ErrorName.Visible) {
66-
This field must not contain invisible characters or newlines
67-
}
68-
@if (error.name === ErrorName.VisibleBlock) {
69-
This field must not contain invisible characters
70-
}
71-
@if (error.name === ErrorName.KebabCase) {
72-
This field must be a valid kebab-case string (e.g. "my-worker")
73-
}
74-
@if (error.name === ErrorName.NameExists) {
75-
Name already taken
76-
}
77-
</div>
78-
}
79-
}
80-
`,
81-
styles: []
43+
templateUrl: './form-error.component.html'
8244
})
8345
export class FormErrorComponent {
8446
public readonly ErrorName = ErrorName;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Component } from '@angular/core';
2+
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
3+
import { RouterLink } from '@angular/router';
4+
import { BehaviorSubject, firstValueFrom } from 'rxjs';
5+
import { AsyncPipe } from '@angular/common';
6+
7+
import { AuthService } from '~/services/auth.service';
8+
import { FormErrorComponent } from '../form-error/form-error.component';
9+
10+
@Component({
11+
imports: [ReactiveFormsModule, FormErrorComponent, RouterLink, AsyncPipe],
12+
template: `
13+
<div class="flex flex-col h-screen items-center justify-center">
14+
<div class="max-w-md p-8">
15+
<h2 class="text-xl font-semibold mb-4">Reset your password</h2>
16+
17+
@if (!(submitted$ | async)) {
18+
<p class="text-gray-500 mb-6">
19+
Enter your email address and we'll send you a link to reset your password.
20+
</p>
21+
22+
<form [formGroup]="form" (ngSubmit)="submitForm()" class="flex flex-col">
23+
<div class="flex flex-col mb-4">
24+
<label class="mb-1 text-sm" for="email">E-mail</label>
25+
<input
26+
class="input-outline mb-0"
27+
type="email"
28+
formControlName="email"
29+
placeholder="you@example.com"
30+
autocomplete="email"
31+
/>
32+
<span class="text-xs pb-2 text-red">
33+
<form-error [control]="form.get('email')"></form-error>
34+
</span>
35+
</div>
36+
37+
<button
38+
class="btn-blue shadow w-full"
39+
[disabled]="form.invalid || (loading$ | async)"
40+
type="submit"
41+
>
42+
@if (loading$ | async) {
43+
<span>Sending...</span>
44+
} @else {
45+
Send reset link
46+
}
47+
</button>
48+
</form>
49+
} @else {
50+
<p class="text-gray-500 mb-6">
51+
{{ message$ | async }}
52+
</p>
53+
}
54+
55+
<a routerLink="/sign-in" class="text-sm text-blue mt-4 block">
56+
Back to Sign in
57+
</a>
58+
</div>
59+
</div>
60+
`
61+
})
62+
export class ForgotPasswordPage {
63+
public readonly form: FormGroup;
64+
public readonly loading$ = new BehaviorSubject<boolean>(false);
65+
public readonly submitted$ = new BehaviorSubject<boolean>(false);
66+
public readonly message$ = new BehaviorSubject<string>('');
67+
68+
constructor(private readonly auth: AuthService) {
69+
this.form = new FormGroup({
70+
email: new FormControl('', [Validators.required, Validators.email])
71+
});
72+
}
73+
74+
public async submitForm(): Promise<void> {
75+
if (this.form.invalid) {
76+
return;
77+
}
78+
79+
this.loading$.next(true);
80+
const { email } = this.form.value;
81+
82+
const result = await firstValueFrom(this.auth.forgotPassword(email));
83+
this.loading$.next(false);
84+
this.submitted$.next(true);
85+
this.message$.next(result.message);
86+
}
87+
}

src/app/modules/login/login.module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ import { provideRoutes } from '~/app/utils/router';
1111
{
1212
path: 'callback/github',
1313
loadComponent: () => import('./callback.page').then((m) => m.CallbackPage)
14+
},
15+
{
16+
path: 'set-password',
17+
loadComponent: () => import('./set-password.page').then((m) => m.SetPasswordPage)
18+
},
19+
{
20+
path: 'forgot-password',
21+
loadComponent: () => import('./forgot-password.page').then((m) => m.ForgotPasswordPage)
22+
},
23+
{
24+
path: 'reset-password',
25+
loadComponent: () => import('./reset-password.page').then((m) => m.ResetPasswordPage)
1426
}
1527
])
1628
]

src/app/modules/login/login.page.html

Lines changed: 128 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
</div>
99
</div>
1010

11-
<!-- Sign in -->
11+
<!-- Sign in / Register -->
1212
<div class="flex flex-col lg:justify-center lg:w-1/3 mx-8 lg:mx-16 min-w-[50%]">
1313
<div class="mx-auto">
1414
<div class="my-12 xs:w-72">
15-
<h2 class="text-xl font-semibold lg:text-2xl">Sign in</h2>
15+
<h2 class="text-xl font-semibold lg:text-2xl">
16+
{{ mode === 'login' ? 'Sign in' : 'Create account' }}
17+
</h2>
1618

1719
<div class="text-gray-500 text-sm max-w-sm">
1820
By signing in you accept our
@@ -24,41 +26,130 @@ <h2 class="text-xl font-semibold lg:text-2xl">Sign in</h2>
2426
@if ({ displayPassForm: false }; as ctx) {
2527
<div>
2628
@if (allowPassword && ctx.displayPassForm) {
27-
<form class="flex flex-col my-12" [formGroup]="form" (ngSubmit)="submitForm()">
28-
<div class="flex flex-col my-2">
29-
<label class="mb-1 text-sm" for="username">E-mail</label>
30-
<input
31-
class="input-outline mb-0"
32-
type="email"
33-
formControlName="username"
34-
placeholder="email"
35-
autocomplete="email"
29+
<!-- Login Form -->
30+
@if (mode === 'login') {
31+
<form class="flex flex-col my-12" [formGroup]="loginForm" (ngSubmit)="submitLogin()">
32+
<div class="flex flex-col my-2">
33+
<label class="mb-1 text-sm" for="email">E-mail</label>
34+
<input
35+
class="input-outline mb-0"
36+
type="email"
37+
formControlName="email"
38+
placeholder="you@example.com"
39+
autocomplete="email"
3640
/>
37-
<span class="text-xs pb-2 text-red">
38-
<form-error [control]="form.get('username')"></form-error>
39-
</span>
40-
</div>
41-
<div class="flex flex-col my-2">
42-
<label class="mb-1 text-sm" for="username">Password</label>
43-
<input
44-
class="input-outline mb-0"
45-
type="password"
46-
formControlName="password"
47-
placeholder="password"
48-
autocomplete="current-password"
41+
<span class="text-xs pb-2 text-red">
42+
<form-error [control]="loginForm.get('email')"></form-error>
43+
</span>
44+
</div>
45+
46+
<div class="flex flex-col my-2">
47+
<label class="mb-1 text-sm" for="password">Password</label>
48+
<input
49+
class="input-outline mb-0"
50+
type="password"
51+
formControlName="password"
52+
placeholder="••••••••"
53+
autocomplete="current-password"
4954
/>
50-
<span class="text-xs pb-2 text-red">
51-
<form-error [control]="form.get('password')"></form-error>
52-
</span>
55+
<span class="text-xs pb-2 text-red">
56+
<form-error [control]="loginForm.get('password')"></form-error>
57+
</span>
58+
</div>
59+
60+
<button
61+
class="btn-blue shadow w-64 mt-2"
62+
[disabled]="loginForm.touched && loginForm.invalid || (loading$ | async)"
63+
type="submit"
64+
>
65+
@if (loading$ | async) {
66+
<span>Signing in...</span>
67+
} @else {
68+
Sign in
69+
}
70+
</button>
71+
72+
@if (error$ | async; as error) {
73+
<p class="text-red text-sm mt-2">{{ error }}</p>
74+
}
75+
76+
<a routerLink="/sign-in/forgot-password" class="text-sm text-blue mt-4">
77+
Forgot password?
78+
</a>
79+
80+
<button
81+
type="button"
82+
class="text-sm text-gray-500 mt-4 text-left"
83+
(click)="toggleMode()"
84+
>
85+
Don't have an account? Sign up
86+
</button>
87+
</form>
88+
}
89+
90+
<!-- Register Form (email only) -->
91+
@if (mode === 'register') {
92+
<div class="flex flex-col my-12">
93+
@if (success$ | async; as success) {
94+
<div class="text-center">
95+
<p class="text-green-600 mb-4">{{ success }}</p>
96+
<button
97+
type="button"
98+
class="text-sm text-gray-500"
99+
(click)="toggleMode()"
100+
>
101+
Back to sign in
102+
</button>
103+
</div>
104+
} @else {
105+
<form [formGroup]="registerForm" (ngSubmit)="submitRegister()">
106+
<div class="flex flex-col my-2">
107+
<label class="mb-1 text-sm" for="email">E-mail</label>
108+
<input
109+
class="input-outline mb-0"
110+
type="email"
111+
formControlName="email"
112+
placeholder="you@example.com"
113+
autocomplete="email"
114+
/>
115+
<span class="text-xs pb-2 text-red">
116+
<form-error [control]="registerForm.get('email')"></form-error>
117+
</span>
118+
</div>
119+
120+
<p class="text-xs text-gray-500 mb-4">
121+
We'll send you an email to set your password.
122+
</p>
123+
124+
<button
125+
class="btn-blue shadow w-64 mt-2"
126+
[disabled]="registerForm.touched && registerForm.invalid || (loading$ | async)"
127+
type="submit"
128+
>
129+
@if (loading$ | async) {
130+
<span>Creating account...</span>
131+
} @else {
132+
Create account
133+
}
134+
</button>
135+
136+
@if (error$ | async; as error) {
137+
<p class="text-red text-sm mt-2">{{ error }}</p>
138+
}
139+
140+
<button
141+
type="button"
142+
class="text-sm text-gray-500 mt-4 text-left"
143+
(click)="toggleMode()"
144+
>
145+
Already have an account? Sign in
146+
</button>
147+
</form>
148+
}
53149
</div>
54-
<button class="btn-blue shadow w-64 mt-2" [disabled]="form.touched && form.invalid" type="submit">
55-
Sign in
56-
</button>
57-
@if (error) {
58-
<p class="text-red">Login failed</p>
59-
}
60-
</form>
150+
}
61151
}
152+
62153
@if (!ctx.displayPassForm) {
63154
<div class="flex flex-col mx-auto my-12">
64155
<form action="/api/v1/openid/github" method="post">
@@ -68,20 +159,21 @@ <h2 class="text-xl font-semibold lg:text-2xl">Sign in</h2>
68159
<path
69160
class="transition-colors dark:fill-white"
70161
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
71-
/>
162+
/>
72163
</svg>
73164
<span>Sign in with Github</span>
74165
</button>
75166
</form>
76167
</div>
77168
}
169+
78170
@if (allowPassword) {
79171
<button
80172
class="text-sm text-gray-500 text-center w-full"
81173
type="button"
82174
(click)="ctx.displayPassForm = !ctx.displayPassForm"
83-
>
84-
Sign in using {{ ctx.displayPassForm ? 'Github' : 'password' }}
175+
>
176+
Sign in using {{ ctx.displayPassForm ? 'Github' : 'email' }}
85177
</button>
86178
}
87179
</div>

0 commit comments

Comments
 (0)