mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Creating controllers, updating deps, and adding more repositories.
This commit is contained in:
parent
16191509b4
commit
3190e9601e
16 changed files with 799 additions and 539 deletions
26
package.json
26
package.json
|
|
@ -25,17 +25,17 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@melt-ui/pp": "^0.3.2",
|
"@melt-ui/pp": "^0.3.2",
|
||||||
"@melt-ui/svelte": "^0.83.0",
|
"@melt-ui/svelte": "^0.83.0",
|
||||||
"@playwright/test": "^1.45.2",
|
"@playwright/test": "^1.45.3",
|
||||||
"@resvg/resvg-js": "^2.6.2",
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"@sveltejs/adapter-auto": "^3.2.2",
|
"@sveltejs/adapter-auto": "^3.2.2",
|
||||||
"@sveltejs/enhanced-img": "^0.3.0",
|
"@sveltejs/enhanced-img": "^0.3.1",
|
||||||
"@sveltejs/kit": "^2.5.18",
|
"@sveltejs/kit": "^2.5.18",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/node": "^20.14.11",
|
"@types/node": "^20.14.13",
|
||||||
"@types/pg": "^8.11.6",
|
"@types/pg": "^8.11.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
||||||
"@typescript-eslint/parser": "^7.16.1",
|
"@typescript-eslint/parser": "^7.17.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"drizzle-kit": "^0.23.0",
|
"drizzle-kit": "^0.23.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
"eslint-plugin-svelte": "^2.43.0",
|
"eslint-plugin-svelte": "^2.43.0",
|
||||||
"just-clone": "^6.2.0",
|
"just-clone": "^6.2.0",
|
||||||
"just-debounce-it": "^3.2.0",
|
"just-debounce-it": "^3.2.0",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.40",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"postcss-load-config": "^5.1.0",
|
"postcss-load-config": "^5.1.0",
|
||||||
"postcss-preset-env": "^9.6.0",
|
"postcss-preset-env": "^9.6.0",
|
||||||
|
|
@ -61,12 +61,12 @@
|
||||||
"sveltekit-flash-message": "^2.4.4",
|
"sveltekit-flash-message": "^2.4.4",
|
||||||
"sveltekit-rate-limiter": "^0.5.2",
|
"sveltekit-rate-limiter": "^0.5.2",
|
||||||
"sveltekit-superforms": "^2.16.1",
|
"sveltekit-superforms": "^2.16.1",
|
||||||
"tailwindcss": "^3.4.6",
|
"tailwindcss": "^3.4.7",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.6.3",
|
||||||
"tsx": "^4.16.2",
|
"tsx": "^4.16.2",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.3.4",
|
"vite": "^5.3.5",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
"@hono/zod-validator": "^0.2.2",
|
"@hono/zod-validator": "^0.2.2",
|
||||||
"@iconify-icons/line-md": "^1.2.30",
|
"@iconify-icons/line-md": "^1.2.30",
|
||||||
"@iconify-icons/mdi": "^1.2.48",
|
"@iconify-icons/mdi": "^1.2.48",
|
||||||
"@internationalized/date": "^3.5.4",
|
"@internationalized/date": "^3.5.5",
|
||||||
"@lucia-auth/adapter-drizzle": "^1.0.7",
|
"@lucia-auth/adapter-drizzle": "^1.0.7",
|
||||||
"@lukeed/uuid": "^2.0.1",
|
"@lukeed/uuid": "^2.0.1",
|
||||||
"@neondatabase/serverless": "^0.9.4",
|
"@neondatabase/serverless": "^0.9.4",
|
||||||
|
|
@ -96,10 +96,10 @@
|
||||||
"cookie": "^0.6.0",
|
"cookie": "^0.6.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"dotenv-expand": "^11.0.6",
|
"dotenv-expand": "^11.0.6",
|
||||||
"drizzle-orm": "^0.32.0",
|
"drizzle-orm": "^0.32.1",
|
||||||
"feather-icons": "^4.29.2",
|
"feather-icons": "^4.29.2",
|
||||||
"formsnap": "^1.0.1",
|
"formsnap": "^1.0.1",
|
||||||
"hono": "^4.5.0",
|
"hono": "^4.5.2",
|
||||||
"hono-rate-limiter": "^0.4.0",
|
"hono-rate-limiter": "^0.4.0",
|
||||||
"html-entities": "^2.5.2",
|
"html-entities": "^2.5.2",
|
||||||
"iconify-icon": "^2.1.0",
|
"iconify-icon": "^2.1.0",
|
||||||
|
|
@ -122,6 +122,6 @@
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.4.0",
|
||||||
"tailwind-variants": "^0.2.1",
|
"tailwind-variants": "^0.2.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod-to-json-schema": "^3.23.1"
|
"zod-to-json-schema": "^3.23.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
900
pnpm-lock.yaml
900
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -4,7 +4,7 @@ import { requireAuth } from "../middleware/auth.middleware";
|
||||||
import { registerEmailPasswordDto } from '$lib/dtos/register-emailpassword.dto';
|
import { registerEmailPasswordDto } from '$lib/dtos/register-emailpassword.dto';
|
||||||
import { limiter } from '../middleware/rate-limiter.middleware';
|
import { limiter } from '../middleware/rate-limiter.middleware';
|
||||||
|
|
||||||
const users = new Hono()
|
const app = new Hono()
|
||||||
.get('/me', requireAuth, async (c) => {
|
.get('/me', requireAuth, async (c) => {
|
||||||
const user = c.var.user;
|
const user = c.var.user;
|
||||||
return c.json({ user });
|
return c.json({ user });
|
||||||
|
|
@ -19,5 +19,4 @@ const users = new Hono()
|
||||||
return c.json({ message: 'Verification email sent' });
|
return c.json({ message: 'Verification email sent' });
|
||||||
});
|
});
|
||||||
|
|
||||||
export default users;
|
export default app;
|
||||||
export type UsersType = typeof users
|
|
||||||
|
|
|
||||||
13
src/lib/server/api/controllers/login.controller.ts
Normal file
13
src/lib/server/api/controllers/login.controller.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import { zValidator } from '@hono/zod-validator';
|
||||||
|
import { registerEmailPasswordDto } from '$lib/dtos/register-emailpassword.dto';
|
||||||
|
import { limiter } from '../middleware/rate-limiter.middleware';
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
.post('/', zValidator('json', registerEmailPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||||
|
const { email } = c.req.valid('json');
|
||||||
|
await loginRequestsService.create({ email });
|
||||||
|
return c.json({ message: 'Verification email sent' });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
0
src/lib/server/api/controllers/signup.controller.ts
Normal file
0
src/lib/server/api/controllers/signup.controller.ts
Normal file
22
src/lib/server/api/controllers/user.controller.ts
Normal file
22
src/lib/server/api/controllers/user.controller.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import { zValidator } from '@hono/zod-validator';
|
||||||
|
import { requireAuth } from "../middleware/auth.middleware";
|
||||||
|
import { registerEmailPasswordDto } from '$lib/dtos/register-emailpassword.dto';
|
||||||
|
import { limiter } from '../middleware/rate-limiter.middleware';
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
.get('/me', requireAuth, async (c) => {
|
||||||
|
const user = c.var.user;
|
||||||
|
return c.json({ user });
|
||||||
|
})
|
||||||
|
.get('/user', requireAuth, async (c) => {
|
||||||
|
const user = c.var.user;
|
||||||
|
return c.json({ user });
|
||||||
|
})
|
||||||
|
.post('/login/request', zValidator('json', registerEmailPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||||
|
const { email } = c.req.valid('json');
|
||||||
|
await this.loginRequestsService.create({ email });
|
||||||
|
return c.json({ message: 'Verification email sent' });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { hc } from 'hono/client';
|
import { hc } from 'hono/client';
|
||||||
|
import { cors } from 'hono/cors';
|
||||||
|
import { logger } from 'hono/logger';
|
||||||
import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware';
|
import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware';
|
||||||
import users from './controllers/iam.controller';
|
import users from './controllers/user.controller';
|
||||||
import { config } from './common/config';
|
import { config } from './common/config';
|
||||||
|
|
||||||
/* ----------------------------------- Api ---------------------------------- */
|
/* ----------------------------------- Api ---------------------------------- */
|
||||||
|
|
@ -9,10 +11,27 @@ const app = new Hono().basePath('/api');
|
||||||
|
|
||||||
/* --------------------------- Global Middlewares --------------------------- */
|
/* --------------------------- Global Middlewares --------------------------- */
|
||||||
app.use(verifyOrigin).use(validateAuthSession);
|
app.use(verifyOrigin).use(validateAuthSession);
|
||||||
|
app.use(logger());
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
'/*',
|
||||||
|
cors({
|
||||||
|
origin: [
|
||||||
|
'http://localhost:5173',
|
||||||
|
'http://localhost:80',
|
||||||
|
'http://host.docker.internal:80',
|
||||||
|
'http://host.docker.internal:5173'
|
||||||
|
], // Replace with your allowed domains
|
||||||
|
|
||||||
|
allowMethods: ['POST'],
|
||||||
|
allowHeaders: ['Content-Type']
|
||||||
|
// credentials: true, // If you need to send cookies or HTTP authentication
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
/* --------------------------------- Routes --------------------------------- */
|
/* --------------------------------- Routes --------------------------------- */
|
||||||
const routes = app
|
const routes = app
|
||||||
.route('/iam', users)
|
.route('/user', users)
|
||||||
.get('/', (c) => c.json({ message: 'Server is healthy' }));
|
.get('/', (c) => c.json({ message: 'Server is healthy' }));
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||||
import { timestamps } from '../utils';
|
import { timestamps } from '../utils';
|
||||||
import user_roles from './userRoles';
|
import user_roles from './userRoles';
|
||||||
|
|
||||||
const usersTable = pgTable('users', {
|
export const usersTable = pgTable('users', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
cuid: text('cuid')
|
cuid: text('cuid')
|
||||||
.unique()
|
.unique()
|
||||||
|
|
@ -25,5 +25,3 @@ export const userRelations = relations(usersTable, ({ many }) => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type Users = InferSelectModel<typeof usersTable>;
|
export type Users = InferSelectModel<typeof usersTable>;
|
||||||
|
|
||||||
export default usersTable;
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { container } from 'tsyringe';
|
// import { lucia } from '../infrastructure/auth/lucia';
|
||||||
import { lucia } from '../infrastructure/auth/lucia';
|
|
||||||
|
|
||||||
// Symbol
|
// // Symbol
|
||||||
export const LuciaProvider = Symbol('LUCIA_PROVIDER');
|
// export const LuciaProvider = Symbol('LUCIA_PROVIDER');
|
||||||
|
|
||||||
// Type
|
// // Type
|
||||||
export type LuciaProvider = typeof lucia;
|
// export type LuciaProvider = typeof lucia;
|
||||||
|
|
||||||
// Register
|
// // Register
|
||||||
container.register<LuciaProvider>(LuciaProvider, { useValue: lucia });
|
// container.register<LuciaProvider>(LuciaProvider, { useValue: lucia });
|
||||||
|
|
|
||||||
56
src/lib/server/api/repositories/users.repository.ts
Normal file
56
src/lib/server/api/repositories/users.repository.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { eq, type InferInsertModel } from 'drizzle-orm';
|
||||||
|
import { usersTable } from '../infrastructure/database/tables/users.table';
|
||||||
|
import { takeFirstOrThrow } from '../infrastructure/database/utils';
|
||||||
|
import { db } from '../infrastructure/database';
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Repository */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* ---------------------------------- About --------------------------------- */
|
||||||
|
/*
|
||||||
|
Repositories are the layer that interacts with the database. They are responsible for retrieving and
|
||||||
|
storing data. They should not contain any business logic, only database queries.
|
||||||
|
*/
|
||||||
|
/* ---------------------------------- Notes --------------------------------- */
|
||||||
|
/*
|
||||||
|
Repositories should only contain methods for CRUD operations and any other database interactions.
|
||||||
|
Any complex logic should be delegated to a service. If a repository method requires a transaction,
|
||||||
|
it should be passed in as an argument or the class should have a method to set the transaction.
|
||||||
|
In our case the method 'trxHost' is used to set the transaction context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type CreateUser = InferInsertModel<typeof usersTable>;
|
||||||
|
export type UpdateUser = Partial<CreateUser>;
|
||||||
|
|
||||||
|
export class UsersRepository {
|
||||||
|
async findOneById(id: string) {
|
||||||
|
return db.query.usersTable.findFirst({
|
||||||
|
where: eq(usersTable.id, id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByIdOrThrow(id: string) {
|
||||||
|
const user = await this.findOneById(id);
|
||||||
|
if (!user) throw Error('User not found');
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByEmail(email: string) {
|
||||||
|
return db.query.usersTable.findFirst({
|
||||||
|
where: eq(usersTable.email, email)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: CreateUser) {
|
||||||
|
return db.insert(usersTable).values(data).returning().then(takeFirstOrThrow);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: UpdateUser) {
|
||||||
|
return db
|
||||||
|
.update(usersTable)
|
||||||
|
.set(data)
|
||||||
|
.where(eq(usersTable.id, id))
|
||||||
|
.returning()
|
||||||
|
.then(takeFirstOrThrow);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/lib/server/api/services/hashing.service.ts
Normal file
32
src/lib/server/api/services/hashing.service.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Argon2id } from "oslo/password";
|
||||||
|
|
||||||
|
/* ---------------------------------- Note ---------------------------------- */
|
||||||
|
/*
|
||||||
|
Reference: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
|
||||||
|
|
||||||
|
I use Scrpt as the hashing algorithm due to its higher compatability
|
||||||
|
with vite's build system and it uses less memory than Argon2id.
|
||||||
|
|
||||||
|
You can use Argon2id or any other hashing algorithm you prefer.
|
||||||
|
*/
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/*
|
||||||
|
With Argon2id, you get the following error at times when vite optimizes its dependencies at times,
|
||||||
|
|
||||||
|
Error: Build failed with 2 errors:
|
||||||
|
node_modules/.pnpm/@node-rs+argon2@1.7.0/node_modules/@node-rs/argon2/index.js:159:36: ERROR: No loader is configured for ".node" files: node_module
|
||||||
|
*/
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
// If you don't use a hasher from oslo, which are preconfigured with recommended parameters from OWASP,
|
||||||
|
// ensure that you configure them properly.
|
||||||
|
export class HashingService {
|
||||||
|
private readonly hasher = new Argon2id();
|
||||||
|
|
||||||
|
async hash(data: string) {
|
||||||
|
return this.hasher.hash(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async verify(hash: string, data: string) {
|
||||||
|
return this.hasher.verify(hash, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/lib/server/api/services/iam.service.ts
Normal file
24
src/lib/server/api/services/iam.service.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { lucia } from '../infrastructure/auth/lucia';
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Service */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* ---------------------------------- About --------------------------------- */
|
||||||
|
/*
|
||||||
|
Services are responsible for handling business logic and data manipulation.
|
||||||
|
They genreally call on repositories or other services to complete a use-case.
|
||||||
|
*/
|
||||||
|
/* ---------------------------------- Notes --------------------------------- */
|
||||||
|
/*
|
||||||
|
Services should be kept as clean and simple as possible.
|
||||||
|
|
||||||
|
Create private functions to handle complex logic and keep the public methods as
|
||||||
|
simple as possible. This makes the service easier to read, test and understand.
|
||||||
|
*/
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
export class IamService {
|
||||||
|
async logout(sessionId: string) {
|
||||||
|
return lucia.invalidateSession(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import { BadRequest } from '../common/errors';
|
|
||||||
import { DatabaseProvider } from '../providers';
|
|
||||||
import { MailerService } from './mailer.service';
|
|
||||||
import { TokensService } from './tokens.service';
|
|
||||||
import { LuciaProvider } from '../providers/lucia.provider';
|
|
||||||
import { UsersRepository } from '../repositories/users.repository';
|
|
||||||
import type { SignInEmailDto } from '../../../dtos/signin-email.dto';
|
|
||||||
import type { RegisterEmailDto } from '../../../dtos/register-email.dto';
|
|
||||||
import { LoginRequestsRepository } from '../repositories/login-requests.repository';
|
|
||||||
|
|
||||||
export class LoginRequestsService {
|
|
||||||
async create(data: RegisterEmailDto) {
|
|
||||||
// generate a token, expiry date, and hash
|
|
||||||
const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm');
|
|
||||||
// save the login request to the database - ensuring we save the hashedToken
|
|
||||||
await this.loginRequestsRepository.create({ email: data.email, hashedToken, expiresAt: expiry });
|
|
||||||
// send the login request email
|
|
||||||
await this.mailerService.sendLoginRequest({
|
|
||||||
to: data.email,
|
|
||||||
props: { token: token }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async verify(data: SignInEmailDto) {
|
|
||||||
const validLoginRequest = await this.fetchValidRequest(data.email, data.token);
|
|
||||||
if (!validLoginRequest) throw BadRequest('Invalid token');
|
|
||||||
|
|
||||||
let existingUser = await this.usersRepository.findOneByEmail(data.email);
|
|
||||||
|
|
||||||
if (!existingUser) {
|
|
||||||
const newUser = await this.handleNewUserRegistration(data.email);
|
|
||||||
return this.lucia.createSession(newUser.id, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.lucia.createSession(existingUser.id, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new user and send a welcome email - or other onboarding process
|
|
||||||
private async handleNewUserRegistration(email: string) {
|
|
||||||
const newUser = await this.usersRepository.create({ email, verified: true, avatar: null })
|
|
||||||
this.mailerService.sendWelcome({ to: email, props: null });
|
|
||||||
// TODO: add whatever onboarding process or extra data you need here
|
|
||||||
return newUser
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch a valid request from the database, verify the token and burn the request if it is valid
|
|
||||||
private async fetchValidRequest(email: string, token: string) {
|
|
||||||
return await this.db.transaction(async (trx) => {
|
|
||||||
// fetch the login request
|
|
||||||
const loginRequest = await this.loginRequestsRepository.trxHost(trx).findOneByEmail(email)
|
|
||||||
if (!loginRequest) return null;
|
|
||||||
|
|
||||||
// check if the token is valid
|
|
||||||
const isValidRequest = await this.tokensService.verifyHashedToken(loginRequest.hashedToken, token);
|
|
||||||
if (!isValidRequest) return null
|
|
||||||
|
|
||||||
// if the token is valid, burn the request
|
|
||||||
await this.loginRequestsRepository.trxHost(trx).deleteById(loginRequest.id);
|
|
||||||
return loginRequest
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
104
src/lib/server/api/services/mailer.service.ts
Normal file
104
src/lib/server/api/services/mailer.service.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import handlebars from 'handlebars';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { injectable } from 'tsyringe';
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Service */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* ---------------------------------- About --------------------------------- */
|
||||||
|
/*
|
||||||
|
Services are responsible for handling business logic and data manipulation.
|
||||||
|
They genreally call on repositories or other services to complete a use-case.
|
||||||
|
*/
|
||||||
|
/* ---------------------------------- Notes --------------------------------- */
|
||||||
|
/*
|
||||||
|
Services should be kept as clean and simple as possible.
|
||||||
|
|
||||||
|
Create private functions to handle complex logic and keep the public methods as
|
||||||
|
simple as possible. This makes the service easier to read, test and understand.
|
||||||
|
*/
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
type SendMail = {
|
||||||
|
to: string | string[];
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SendTemplate<T> = {
|
||||||
|
to: string | string[];
|
||||||
|
props: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class MailerService {
|
||||||
|
private nodemailer = nodemailer.createTransport({
|
||||||
|
host: 'smtp.ethereal.email',
|
||||||
|
port: 587,
|
||||||
|
secure: false, // Use `true` for port 465, `false` for all other ports
|
||||||
|
auth: {
|
||||||
|
user: 'adella.hoppe@ethereal.email',
|
||||||
|
pass: 'dshNQZYhATsdJ3ENke'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sendEmailVerificationToken(data: SendTemplate<{ token: string }>) {
|
||||||
|
const template = handlebars.compile(this.getTemplate('email-verification-token'));
|
||||||
|
return this.send({
|
||||||
|
to: data.to,
|
||||||
|
subject: 'Email Verification',
|
||||||
|
html: template({ token: data.props.token })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEmailChangeNotification(data: SendTemplate<null>) {
|
||||||
|
const template = handlebars.compile(this.getTemplate('email-change-notice'));
|
||||||
|
return this.send({
|
||||||
|
to: data.to,
|
||||||
|
subject: 'Email Change Notice',
|
||||||
|
html: template(null)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendLoginRequest(data: SendTemplate<{ token: string }>) {
|
||||||
|
const template = handlebars.compile(this.getTemplate('email-verification-token'));
|
||||||
|
return this.send({
|
||||||
|
to: data.to,
|
||||||
|
subject: 'Login Request',
|
||||||
|
html: template({ token: data.props.token })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendWelcome(data: SendTemplate<null>) {
|
||||||
|
const template = handlebars.compile(this.getTemplate('welcome'));
|
||||||
|
return this.send({
|
||||||
|
to: data.to,
|
||||||
|
subject: 'Welcome!',
|
||||||
|
html: template(null)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async send({ to, subject, html }: SendMail) {
|
||||||
|
const message = await this.nodemailer.sendMail({
|
||||||
|
from: '"Example" <example@ethereal.email>', // sender address
|
||||||
|
bcc: to,
|
||||||
|
subject, // Subject line
|
||||||
|
text: html,
|
||||||
|
html
|
||||||
|
});
|
||||||
|
console.log(nodemailer.getTestMessageUrl(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTemplate(template: string) {
|
||||||
|
const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
|
||||||
|
const __dirname = path.dirname(__filename); // get the name of the directory
|
||||||
|
return fs.readFileSync(
|
||||||
|
path.join(__dirname, `../infrastructure/email-templates/${template}.hbs`),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/lib/server/api/services/queues.service.ts
Normal file
19
src/lib/server/api/services/queues.service.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { injectable } from "tsyringe";
|
||||||
|
import RedisClient from 'ioredis'
|
||||||
|
import { config } from "../common/config";
|
||||||
|
import { Queue, Worker, type Processor } from 'bullmq';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class QueuesServices {
|
||||||
|
connection = new RedisClient(config.REDIS_URL);
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
createQueue(name: string) {
|
||||||
|
return new Queue(name, { connection: this.connection })
|
||||||
|
}
|
||||||
|
|
||||||
|
createWorker(name: string, prcoessor: Processor) {
|
||||||
|
return new Worker(name, prcoessor, { connection: this.connection })
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/lib/server/api/services/tokens.service.ts
Normal file
33
src/lib/server/api/services/tokens.service.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { generateRandomString } from "oslo/crypto";
|
||||||
|
import { TimeSpan, createDate, type TimeSpanUnit } from 'oslo';
|
||||||
|
import { HashingService } from "./hashing.service";
|
||||||
|
|
||||||
|
export class TokensService {
|
||||||
|
private readonly hashingService = new HashingService();
|
||||||
|
|
||||||
|
generateToken() {
|
||||||
|
const alphabet = '23456789ACDEFGHJKLMNPQRSTUVWXYZ'; // alphabet with removed look-alike characters (0, 1, O, I)
|
||||||
|
return generateRandomString(6, alphabet);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateTokenWithExpiry(number: number, lifespan: TimeSpanUnit) {
|
||||||
|
return {
|
||||||
|
token: this.generateToken(),
|
||||||
|
expiry: createDate(new TimeSpan(number, lifespan))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateTokenWithExpiryAndHash(number: number, lifespan: TimeSpanUnit) {
|
||||||
|
const token = this.generateToken()
|
||||||
|
const hashedToken = await this.hashingService.hash(token)
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
hashedToken,
|
||||||
|
expiry: createDate(new TimeSpan(number, lifespan))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyHashedToken(hashedToken: string, token: string) {
|
||||||
|
return this.hashingService.verify(hashedToken, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue