Skip to content

Commit b33590e

Browse files
committed
refactored auth services
1 parent 80ba1c9 commit b33590e

File tree

13 files changed

+215
-110
lines changed

13 files changed

+215
-110
lines changed

src/app.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ declare global {
1212
api: ApiClient['api'];
1313
parseApiResponse: typeof parseApiResponse;
1414
getAuthedUser: () => Promise<Returned<User> | null>;
15-
getAuthedUserOrThrow: () => Promise<Returned<User>>;
15+
getAuthedUserOrThrow: (redirectTo: string) => Promise<Returned<User>>;
1616
}
1717

1818
// interface PageData {}

src/hooks.server.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ const apiClient: Handle = async ({ event, resolve }) => {
1818

1919
/* ----------------------------- Auth functions ----------------------------- */
2020
async function getAuthedUser() {
21-
const { data } = await api.iam.user.$get().then(parseApiResponse)
21+
const { data } = await api.users.me.$get().then(parseApiResponse)
2222
return data && data.user;
2323
}
2424

25-
async function getAuthedUserOrThrow() {
26-
const { data } = await api.iam.user.$get().then(parseApiResponse);
27-
if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/');
25+
async function getAuthedUserOrThrow(redirectTo = '/') {
26+
const { data } = await api.users.me.$get().then(parseApiResponse);
27+
if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, redirectTo);
2828
return data?.user;
2929
}
3030

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,41 @@
11
import { setCookie } from 'hono/cookie';
22
import { inject, injectable } from 'tsyringe';
33
import { zValidator } from '@hono/zod-validator';
4-
import { IamService } from '../services/iam.service';
54
import { limiter } from '../middlewares/rate-limiter.middlware';
6-
import { requireAuth } from '../middlewares/auth.middleware';
5+
import { requireAuth } from '../middlewares/require-auth.middleware';
76
import { Controler } from '../common/types/controller';
8-
import { registerEmailDto } from '$lib/server/api/dtos/register-email.dto';
9-
import { signInEmailDto } from '$lib/server/api/dtos/signin-email.dto';
107
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
118
import { verifyEmailDto } from '$lib/server/api/dtos/verify-email.dto';
129
import { LuciaService } from '../services/lucia.service';
10+
import { AuthenticationService } from '../services/authentication.service';
11+
import { EmailVerificationService } from '../services/email-verification.service';
12+
import { loginDto } from '../dtos/login.dto';
13+
import { verifyLoginDto } from '../dtos/verify-login.dto';
1314

1415
@injectable()
1516
export class IamController extends Controler {
1617
constructor(
17-
@inject(IamService) private iamService: IamService,
18+
@inject(AuthenticationService) private authenticationService: AuthenticationService,
19+
@inject(EmailVerificationService) private emailVerificationService: EmailVerificationService,
1820
@inject(LuciaService) private luciaService: LuciaService,
1921
) {
2022
super();
2123
}
2224

2325
routes() {
2426
return this.controller
25-
.get('/user', async (c) => {
27+
.get('/me', async (c) => {
2628
const user = c.var.user;
2729
return c.json({ user: user });
2830
})
29-
.post('/login/request', zValidator('json', registerEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
31+
.post('/login', zValidator('json', loginDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
3032
const { email } = c.req.valid('json');
31-
await this.iamService.createLoginRequest({ email });
33+
await this.authenticationService.createLoginRequest({ email });
3234
return c.json({ message: 'Verification email sent' });
3335
})
34-
.post('/login/verify', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
36+
.post('/login/verify', zValidator('json', verifyLoginDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
3537
const { email, token } = c.req.valid('json');
36-
const session = await this.iamService.verifyLoginRequest({ email, token });
38+
const session = await this.authenticationService.verifyLoginRequest({ email, token });
3739
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id);
3840
setCookie(c, sessionCookie.name, sessionCookie.value, {
3941
path: sessionCookie.attributes.path,
@@ -46,9 +48,21 @@ export class IamController extends Controler {
4648
});
4749
return c.json({ message: 'ok' });
4850
})
51+
.patch('/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
52+
const json = c.req.valid('json');
53+
await this.emailVerificationService.create(c.var.user.id, json.email);
54+
return c.json({ message: 'Verification email sent' });
55+
})
56+
// this could also be named to use custom methods, aka /email#verify
57+
// https://cloud.google.com/apis/design/custom_methods
58+
.post('/email/verify', requireAuth, zValidator('json', verifyEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
59+
const json = c.req.valid('json');
60+
await this.emailVerificationService.verify(c.var.user.id, json.token);
61+
return c.json({ message: 'Verified and updated' });
62+
})
4963
.post('/logout', requireAuth, async (c) => {
5064
const sessionId = c.var.session.id;
51-
await this.iamService.logout(sessionId);
65+
await this.authenticationService.logout(sessionId);
5266
const sessionCookie = this.luciaService.lucia.createBlankSessionCookie();
5367
setCookie(c, sessionCookie.name, sessionCookie.value, {
5468
path: sessionCookie.attributes.path,
@@ -61,17 +75,5 @@ export class IamController extends Controler {
6175
});
6276
return c.json({ status: 'success' });
6377
})
64-
.patch('/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
65-
const json = c.req.valid('json');
66-
await this.iamService.dispatchEmailVerificationRequest(c.var.user.id, json.email);
67-
return c.json({ message: 'Verification email sent' });
68-
})
69-
// this could also be named to use custom methods, aka /email#verify
70-
// https://cloud.google.com/apis/design/custom_methods
71-
.post('/email/verification', requireAuth, zValidator('json', verifyEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
72-
const json = c.req.valid('json');
73-
await this.iamService.processEmailVerificationRequest(c.var.user.id, json.token);
74-
return c.json({ message: 'Verified and updated' });
75-
});
7678
}
7779
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { z } from 'zod';
2+
3+
export const loginDto = z.object({
4+
email: z.string().email()
5+
});
6+
7+
export type LoginDto = z.infer<typeof loginDto>;

src/lib/server/api/dtos/register-email.dto.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/lib/server/api/dtos/signin-email.dto.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { z } from 'zod';
2+
3+
export const verifyLoginDto = z.object({
4+
email: z.string().email(),
5+
token: z.string()
6+
});
7+
8+
export type VerifyLoginDto = z.infer<typeof verifyLoginDto>;

src/lib/server/api/middlewares/auth.middleware.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import type { MiddlewareHandler } from 'hono';
22
import { createMiddleware } from 'hono/factory';
33
import { verifyRequestOrigin } from 'lucia';
4-
import type { Session, User } from 'lucia';
5-
import { Unauthorized } from '../common/exceptions';
64
import type { HonoTypes } from '../common/types/hono';
75
import { container } from 'tsyringe';
86
import { LuciaService } from '../services/lucia.service';
@@ -41,15 +39,4 @@ export const validateAuthSession: MiddlewareHandler<HonoTypes> = createMiddlewar
4139
c.set("session", session);
4240
c.set("user", user);
4341
return next();
44-
})
45-
46-
export const requireAuth: MiddlewareHandler<{
47-
Variables: {
48-
session: Session;
49-
user: User;
50-
};
51-
}> = createMiddleware(async (c, next) => {
52-
const user = c.var.user;
53-
if (!user) throw Unauthorized('You must be logged in to access this resource');
54-
return next();
55-
});
42+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { MiddlewareHandler } from "hono";
2+
import { createMiddleware } from "hono/factory";
3+
import type { Session, User } from "lucia";
4+
import { Unauthorized } from "../common/exceptions";
5+
6+
export const requireAuth: MiddlewareHandler<{
7+
Variables: {
8+
session: Session;
9+
user: User;
10+
};
11+
}> = createMiddleware(async (c, next) => {
12+
const user = c.var.user;
13+
if (!user) throw Unauthorized('You must be logged in to access this resource');
14+
return next();
15+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { inject, injectable } from 'tsyringe';
2+
import { MailerService } from './mailer.service';
3+
import { TokensService } from './tokens.service';
4+
import { UsersRepository } from '../repositories/users.repository';
5+
import type { VerifyLoginDto } from '../dtos/verify-login.dto';
6+
import type { LoginDto } from '../dtos/login.dto';
7+
import { LoginRequestsRepository } from '../repositories/login-requests.repository';
8+
import { LoginVerificationEmail } from '../emails/login-verification.email';
9+
import { BadRequest } from '../common/exceptions';
10+
import { WelcomeEmail } from '../emails/welcome.email';
11+
import { DrizzleService } from './drizzle.service';
12+
import { LuciaService } from './lucia.service';
13+
14+
@injectable()
15+
export class AuthenticationService {
16+
constructor(
17+
@inject(LuciaService) private readonly luciaService: LuciaService,
18+
@inject(DrizzleService) private readonly drizzleService: DrizzleService,
19+
@inject(TokensService) private readonly tokensService: TokensService,
20+
@inject(MailerService) private readonly mailerService: MailerService,
21+
@inject(UsersRepository) private readonly usersRepository: UsersRepository,
22+
@inject(LoginRequestsRepository) private readonly loginRequestsRepository: LoginRequestsRepository,
23+
) { }
24+
25+
async createLoginRequest(data: LoginDto) {
26+
// generate a token, expiry date, and hash
27+
const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm');
28+
// save the login request to the database - ensuring we save the hashedToken
29+
await this.loginRequestsRepository.create({ email: data.email, hashedToken, expiresAt: expiry });
30+
// send the login request email
31+
await this.mailerService.send({ email: new LoginVerificationEmail(token), to: data.email });
32+
}
33+
34+
async verifyLoginRequest(data: VerifyLoginDto) {
35+
const validLoginRequest = await this.getValidLoginRequest(data.email, data.token);
36+
if (!validLoginRequest) throw BadRequest('Invalid token');
37+
38+
let existingUser = await this.usersRepository.findOneByEmail(data.email);
39+
40+
if (!existingUser) {
41+
const newUser = await this.handleNewUserRegistration(data.email);
42+
return this.luciaService.lucia.createSession(newUser.id, {});
43+
}
44+
45+
return this.luciaService.lucia.createSession(existingUser.id, {});
46+
}
47+
48+
async logout(sessionId: string) {
49+
return this.luciaService.lucia.invalidateSession(sessionId);
50+
}
51+
52+
// Create a new user and send a welcome email - or other onboarding process
53+
private async handleNewUserRegistration(email: string) {
54+
const newUser = await this.usersRepository.create({ email, verified: true })
55+
await this.mailerService.send({ email: new WelcomeEmail(), to: newUser.email });
56+
// TODO: add whatever onboarding process or extra data you need here
57+
return newUser
58+
}
59+
60+
// Fetch a valid request from the database, verify the token and burn the request if it is valid
61+
private async getValidLoginRequest(email: string, token: string) {
62+
return await this.drizzleService.db.transaction(async (trx) => {
63+
// fetch the login request
64+
const loginRequest = await this.loginRequestsRepository.findOneByEmail(email, trx)
65+
if (!loginRequest) return null;
66+
67+
// check if the token is valid
68+
const isValidRequest = await this.tokensService.verifyHashedToken(loginRequest.hashedToken, token);
69+
if (!isValidRequest) return null
70+
71+
// if the token is valid, burn the request
72+
await this.loginRequestsRepository.deleteById(loginRequest.id, trx);
73+
return loginRequest
74+
})
75+
}
76+
77+
78+
79+
80+
}

0 commit comments

Comments
 (0)