Skip to content

Commit 5a2ca02

Browse files
committed
feat(web): add all new auth/user pages
Add all new authentication/user-related pages. This commit includes: - Svelte-based login page - Finalize account page - Signup page - Resend confirmation email page - Password reset start and confirmation pages Each page handles its specific logic, error display, and integrates with LoginService or the OAuth buttons as required. Navigation links, redirects, and user states are managed according to the new auth flow.
1 parent d8f7184 commit 5a2ca02

File tree

7 files changed

+1149
-1
lines changed

7 files changed

+1149
-1
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<script lang="ts">
2+
import RedirectIfLoggedIn from '$lib/auth/RedirectIfLoggedIn.svelte';
3+
import { AUTH_SERVICE } from '$lib/auth/authService.svelte';
4+
import GitHubButton from '$lib/components/login/GitHubButton.svelte';
5+
import GoogleButton from '$lib/components/login/GoogleButton.svelte';
6+
import { inject } from '@gitbutler/shared/context';
7+
import { LOGIN_SERVICE } from '@gitbutler/shared/login/loginService';
8+
import { WEB_ROUTES_SERVICE } from '@gitbutler/shared/routing/webRoutes.svelte';
9+
import { Button, SectionCard } from '@gitbutler/ui';
10+
import { env } from '$env/dynamic/public';
11+
12+
let email = $state<string>();
13+
let password = $state<string>();
14+
15+
let error = $state<string>();
16+
let errorCode = $state<string>();
17+
18+
const loginService = inject(LOGIN_SERVICE);
19+
const routesService = inject(WEB_ROUTES_SERVICE);
20+
const authService = inject(AUTH_SERVICE);
21+
22+
async function handleSubmit(event: Event) {
23+
event.preventDefault();
24+
if (!email || !password) {
25+
console.error('Email and password are required');
26+
return;
27+
}
28+
29+
const response = await loginService.loginWithEmail(email, password);
30+
31+
if (response.type === 'error') {
32+
error = response.errorMessage;
33+
errorCode = response.errorCode;
34+
console.error('Login failed:', response.raw ?? response.errorMessage);
35+
} else {
36+
const token = response.data;
37+
authService.setToken(token);
38+
window.location.href = `${env.PUBLIC_APP_HOST}successful_login?access_token=${token}`;
39+
}
40+
}
41+
42+
async function resendConfirmationEmail() {
43+
if (!email) {
44+
error = 'Please enter your email to resend the confirmation email.';
45+
return;
46+
}
47+
const response = await loginService.resendConfirmationEmail(email);
48+
if (response.type === 'error') {
49+
error = response.errorMessage;
50+
errorCode = response.errorCode;
51+
console.error('Failed to resend confirmation email:', response.raw ?? response.errorMessage);
52+
} else {
53+
error = 'Confirmation email resent. Please check your inbox.';
54+
}
55+
}
56+
</script>
57+
58+
<svelte:head>
59+
<title>GitButler | Login</title>
60+
</svelte:head>
61+
62+
<RedirectIfLoggedIn />
63+
64+
<div class="main-links">
65+
<a href={routesService.homePath()} class="logo" aria-label="main nav" title="Home">
66+
<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
67+
<path d="M0 22V0L11.4819 9.63333L23 0V22L11.4819 12.4L0 22Z" fill="var(--clr-text-1)" />
68+
</svg>
69+
</a>
70+
</div>
71+
72+
<form onsubmit={handleSubmit} class="login-form">
73+
<div class="login-form__content">
74+
<SectionCard>
75+
<div class="field">
76+
<label for="email">Email</label>
77+
<input id="email" type="email" bind:value={email} required />
78+
</div>
79+
<div class="field">
80+
<label for="password">Password</label>
81+
<input id="password" type="password" bind:value={password} required />
82+
</div>
83+
</SectionCard>
84+
85+
<div class="reset-password-link">
86+
Or did you forget your password?
87+
<br />
88+
Wow. That sounds irresponsible.
89+
<a href={routesService.resetPasswordPath()}>I mean... it's fine. Reset it</a>
90+
</div>
91+
92+
<Button type="submit">Log in</Button>
93+
94+
<div class="signup-link">
95+
Don't have an account?
96+
<a href={routesService.signupPath()}>Sign up</a>
97+
</div>
98+
99+
{#if error}
100+
<div class="error-message">
101+
<span>
102+
{error}
103+
</span>
104+
</div>
105+
{#if errorCode === 'email_not_verified'}
106+
<span>
107+
Please verify your email address before logging in. Check your inbox for a verification
108+
email or
109+
</span>
110+
<Button
111+
type="button"
112+
onclick={resendConfirmationEmail}
113+
disabled={!email}
114+
tooltip={!email
115+
? 'Please enter your email in the field above to resend the confirmation email.'
116+
: 'Resend confirmation email'}
117+
>
118+
resend the confirmation email</Button
119+
>
120+
{/if}
121+
{/if}
122+
123+
<SectionCard>
124+
<GitHubButton />
125+
<GoogleButton />
126+
</SectionCard>
127+
</div>
128+
</form>
129+
130+
<style lang="postcss">
131+
.main-links {
132+
display: flex;
133+
align-items: center;
134+
margin-bottom: 16px;
135+
overflow: hidden;
136+
gap: 16px;
137+
}
138+
139+
.logo {
140+
display: flex;
141+
}
142+
143+
.login-form__content {
144+
display: flex;
145+
flex-direction: column;
146+
max-width: 400px;
147+
margin: auto;
148+
gap: 16px;
149+
}
150+
151+
.field {
152+
display: flex;
153+
flex-direction: column;
154+
gap: 4px;
155+
156+
label {
157+
color: var(--clr-scale-ntrl-30);
158+
font-size: 14px;
159+
}
160+
161+
input {
162+
padding: 8px 12px;
163+
border: 1px solid var(--clr-border-2);
164+
border-radius: var(--radius-m);
165+
background-color: var(--clr-bg-1);
166+
color: var(--clr-scale-ntrl-0);
167+
font-size: 14px;
168+
169+
&:read-only {
170+
cursor: not-allowed;
171+
opacity: 0.7;
172+
}
173+
174+
&:not(:read-only) {
175+
&:focus {
176+
border-color: var(--clr-scale-pop-70);
177+
outline: none;
178+
}
179+
}
180+
}
181+
}
182+
183+
.error-message {
184+
display: flex;
185+
flex-direction: column;
186+
padding: 8px;
187+
gap: 8px;
188+
border: 1px solid var(--clr-scale-err-60);
189+
border-radius: var(--radius-m);
190+
191+
background-color: var(--clr-theme-err-bg-muted);
192+
color: var(--clr-scale-err-10);
193+
font-size: 14px;
194+
}
195+
196+
.signup-link,
197+
.reset-password-link {
198+
display: flex;
199+
flex-direction: column;
200+
align-items: center;
201+
justify-content: center;
202+
203+
gap: 4px;
204+
font-size: 14px;
205+
206+
a {
207+
text-decoration: underline;
208+
}
209+
}
210+
</style>
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<script lang="ts">
2+
import { page } from '$app/state';
3+
import RedirectIfLoggedIn from '$lib/auth/RedirectIfLoggedIn.svelte';
4+
import { AUTH_SERVICE } from '$lib/auth/authService.svelte';
5+
import { inject } from '@gitbutler/shared/context';
6+
import { LOGIN_SERVICE } from '@gitbutler/shared/login/loginService';
7+
import { WEB_ROUTES_SERVICE } from '@gitbutler/shared/routing/webRoutes.svelte';
8+
import { Button, SectionCard } from '@gitbutler/ui';
9+
import { env } from '$env/dynamic/public';
10+
11+
const loginService = inject(LOGIN_SERVICE);
12+
const routesService = inject(WEB_ROUTES_SERVICE);
13+
const authService = inject(AUTH_SERVICE);
14+
15+
let password = $state<string>();
16+
let passwordConfirmation = $state<string>();
17+
let error = $state<string>();
18+
let message = $state<string>();
19+
20+
const confirmationMatches = $derived(password === passwordConfirmation);
21+
22+
async function handleSubmit() {
23+
const token = page.url.searchParams.get('t');
24+
25+
if (!token) {
26+
error = 'Invalid or missing token';
27+
// TODO: Probably redirect to the login page or show a more user-friendly error
28+
return;
29+
}
30+
31+
if (!confirmationMatches) {
32+
error = 'Passwords do not match';
33+
return;
34+
}
35+
36+
if (!password || !passwordConfirmation) {
37+
error = 'Password are required';
38+
return;
39+
}
40+
41+
const response = await loginService.confirmPasswordReset(token, password, passwordConfirmation);
42+
if (response.type === 'error') {
43+
error = response.errorMessage;
44+
console.error('Confirm password failed:', response.raw ?? response.errorMessage);
45+
message = '';
46+
return;
47+
}
48+
49+
error = undefined;
50+
message = response.data.message;
51+
authService.setToken(response.data.token);
52+
window.location.href = `${env.PUBLIC_APP_HOST}successful_login?access_token=${token}`;
53+
}
54+
</script>
55+
56+
<svelte:head>
57+
<title>GitButler | Confirm Password</title>
58+
</svelte:head>
59+
60+
<RedirectIfLoggedIn />
61+
62+
<div class="main-links">
63+
<a href={routesService.homePath()} class="logo" aria-label="main nav" title="Home">
64+
<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
65+
<path d="M0 22V0L11.4819 9.63333L23 0V22L11.4819 12.4L0 22Z" fill="var(--clr-text-1)" />
66+
</svg>
67+
</a>
68+
</div>
69+
70+
<form onsubmit={handleSubmit} class="signin-form">
71+
<div class="signup-form__content">
72+
<SectionCard>
73+
<div class="field">
74+
<label for="password">Password</label>
75+
<input
76+
id="password"
77+
type="password"
78+
bind:value={password}
79+
required
80+
autocomplete="new-password"
81+
/>
82+
</div>
83+
<div class="field">
84+
<label for="passwordConfirmation">Password confirmation</label>
85+
<input
86+
id="passwordConfirmation"
87+
type="password"
88+
bind:value={passwordConfirmation}
89+
required
90+
autocomplete="new-password"
91+
/>
92+
</div>
93+
</SectionCard>
94+
95+
<Button type="submit">Log in</Button>
96+
97+
{#if error}
98+
<div class="error-message">{error}</div>
99+
{/if}
100+
101+
{#if message}
102+
<div class="message">{message}</div>
103+
{/if}
104+
</div>
105+
</form>
106+
107+
<style lang="postcss">
108+
.main-links {
109+
display: flex;
110+
align-items: center;
111+
margin-bottom: 16px;
112+
overflow: hidden;
113+
gap: 16px;
114+
}
115+
116+
.logo {
117+
display: flex;
118+
}
119+
120+
.signup-form__content {
121+
display: flex;
122+
flex-direction: column;
123+
max-width: 400px;
124+
margin: auto;
125+
gap: 16px;
126+
}
127+
128+
.field {
129+
display: flex;
130+
flex-direction: column;
131+
gap: 4px;
132+
133+
label {
134+
color: var(--clr-scale-ntrl-30);
135+
font-size: 14px;
136+
}
137+
138+
input {
139+
padding: 8px 12px;
140+
border: 1px solid var(--clr-border-2);
141+
border-radius: var(--radius-m);
142+
background-color: var(--clr-bg-1);
143+
color: var(--clr-scale-ntrl-0);
144+
font-size: 14px;
145+
146+
&:read-only {
147+
cursor: not-allowed;
148+
opacity: 0.7;
149+
}
150+
151+
&:not(:read-only) {
152+
&:focus {
153+
border-color: var(--clr-scale-pop-70);
154+
outline: none;
155+
}
156+
}
157+
}
158+
}
159+
160+
.error-message {
161+
padding: 8px;
162+
border: 1px solid var(--clr-scale-err-60);
163+
border-radius: var(--radius-m);
164+
background-color: var(--clr-theme-err-bg-muted);
165+
color: var(--clr-scale-err-10);
166+
font-size: 14px;
167+
}
168+
169+
.message {
170+
padding: 8px;
171+
border: 1px solid var(--clr-scale-succ-60);
172+
border-radius: var(--radius-m);
173+
background-color: var(--clr-theme-succ-bg-muted);
174+
color: var(--clr-scale-err-10);
175+
font-size: 14px;
176+
}
177+
</style>

0 commit comments

Comments
 (0)