mirror of
https://github.com/BradNut/TofuStack
synced 2025-09-08 17:40:26 +00:00
Removed fragmented controllers in favor of hono's native chaining
This commit is contained in:
parent
163b8bdc52
commit
653e2c22cf
20 changed files with 354 additions and 149 deletions
45
README.md
45
README.md
|
|
@ -1,4 +1,4 @@
|
|||
# Sveltekit - Starter BYOB (Bring your own Backend)
|
||||
# Sveltekit - Starter BYOT (Bring your own Tech)
|
||||
|
||||
A scalable, testable, extensible, boilerplate for Sveltekit.
|
||||
|
||||
|
|
@ -24,7 +24,24 @@ export const DELETE: RequestHandler = ({ request }) => app.fetch(request);
|
|||
export const POST: RequestHandler = ({ request }) => app.fetch(request);
|
||||
```
|
||||
|
||||
## Library Selection
|
||||
## Features
|
||||
|
||||
- [x] Email
|
||||
- [x] E2E Typesafety
|
||||
- [x] Base Test Coverage
|
||||
- [x] Authentication
|
||||
- [x] Email/Passkey
|
||||
- [ ] OAuth (Implementation varies so maybe not included as base)
|
||||
- [x] Email verification
|
||||
- [x] Email updating
|
||||
- [x] Database
|
||||
- [x] Migrations
|
||||
|
||||
## Who is this for?
|
||||
|
||||
Me. I created this for myself to bootstrap weekend projects. Its a continous work in progress that I'd like to eventually make ready for public use. That being said, if you see something I could improve I welcome feedback/prs.
|
||||
|
||||
## Opinionated Library Selection
|
||||
|
||||
My selection of libraries are just what I've found success, **stable** and high cohesion with. This repo should just serve as a guide as to whats possible; not what libary is the best.
|
||||
|
||||
|
|
@ -42,26 +59,28 @@ My selection of libraries are just what I've found success, **stable** and high
|
|||
|
||||
#### Frontend
|
||||
|
||||
- **[Sveltekit](https://kit.svelte.dev/)**: After trying Vue, React, Next, and pretty much every frotnend framework in the JS ecosystem, its safe to say I vastly prefer Svelte and its priority of building on web standards.
|
||||
- **[Sveltekit](https://kit.svelte.dev/)**: After trying Vue, React, Next, and pretty much every frotnend framework in the JS ecosystem, its safe to say I vastly prefer Svelte and its priority of building on web standards.
|
||||
|
||||
#### Dependency Injection
|
||||
|
||||
- **[TSyringe](https://github.com/microsoft/tsyringe)**: Lightweight dependency injection for JS/TS. If you're familiar with TypeDI, this is essentially the same but actively maintained and used by Microsoft.
|
||||
|
||||
## Architecture
|
||||
## Architecture
|
||||
|
||||
We have a few popular architectures for structuring backends
|
||||
* Technical
|
||||
* Clean/Onion
|
||||
* Domain Driven Design(DDD)/Vertical Slice Architecture(VSA)
|
||||
We have a few popular architectures for structuring traditional backends
|
||||
|
||||
I choose to start with organizing my backends by technical imeplementions and evolve into one of the others as the project evolves. You don't have to strictly stick to any specific architecture, but they do serve as good guidelines. Alternatively, move things around until it feels right.
|
||||
- Technical
|
||||
- Clean/Onion
|
||||
- Domain Driven Design(DDD)/Vertical Slice Architecture(VSA)
|
||||
|
||||
I choose to start with organizing my backends by **Technical** imeplementions and evolve into one of the others as the project increases in complexity. You don't have to strictly stick to any specific architecture, but they do serve as good guidelines. Alternatively, move things around until it feels right.
|
||||
|
||||
## Abstraction
|
||||
|
||||
Too many boilerplates lock you into technology choices or straight up throw random libraries to advertise they have more features than their competitors. This is a horrific practice. Just look at the feature list of this ["boilerplate for NextJS"](https://github.com/ixartz/Next-js-Boilerplate#getting-started). The instant this boiler it used, your code is already complex, riddled with external services, impossible to test, require signing up with multiple external services, AND YOU HAVENT EVEN STARTED DEVELOPING YOUR PRODUCT.
|
||||
|
||||
## Testing
|
||||
|
||||
Testing probably isn't first and foremost when creating an app. Thats fine. You shoulkdn't be spending time writing tests if your app is changing and pivoting.
|
||||
|
||||
BUT a good stack should be **testable** when the time to solidify a codebase arrives. I created this stack with that pinciple in mind. I've provided a examples of how to write these tests under `authentication.service.test.ts` or `users.service.test.ts`
|
||||
|
||||
Testing probably isn't first and foremost when creating an app. Thats fine. You probably shouldnt't be spending time writing tests if your app is changing and pivoting.
|
||||
|
||||
BUT, a good stack should be **testable** when the time to solidify a codebase arrives. I created this stack with that pinciple in mind. I've provided a examples of how to write these tests under `iam.service.test.ts`.
|
||||
|
|
|
|||
4
src/app.d.ts
vendored
4
src/app.d.ts
vendored
|
|
@ -8,8 +8,8 @@ declare global {
|
|||
// interface Error {}
|
||||
interface Locals {
|
||||
api: ApiClient['api'];
|
||||
getAuthedUser: () => Promise<User | null>;
|
||||
getAuthedUserOrThrow: () => Promise<User>;
|
||||
getAuthedUser: () => Promise<Returned<User> | null>;
|
||||
getAuthedUserOrThrow: () => Promise<Returned<User>>;
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { hc, type ClientResponse } from 'hono/client';
|
||||
import { hc } from 'hono/client';
|
||||
import { redirect, type Handle } from '@sveltejs/kit';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import type { ApiRoutes } from '$lib/server/api';
|
||||
import { parseApiResponse } from '$lib/helpers';
|
||||
|
||||
const apiClient: Handle = async ({ event, resolve }) => {
|
||||
/* ------------------------------ Register api ------------------------------ */
|
||||
const { api } = hc<ApiRoutes>('/', {
|
||||
fetch: event.fetch,
|
||||
headers: {
|
||||
|
|
@ -12,6 +13,7 @@ const apiClient: Handle = async ({ event, resolve }) => {
|
|||
}
|
||||
});
|
||||
|
||||
/* ----------------------------- Auth functions ----------------------------- */
|
||||
async function getAuthedUser() {
|
||||
const { data } = await parseApiResponse(api.iam.user.$get());
|
||||
return data && data.user;
|
||||
|
|
@ -23,11 +25,12 @@ const apiClient: Handle = async ({ event, resolve }) => {
|
|||
return data?.user;
|
||||
}
|
||||
|
||||
// set contexts
|
||||
/* ------------------------------ Set contexts ------------------------------ */
|
||||
event.locals.api = api;
|
||||
event.locals.getAuthedUser = getAuthedUser;
|
||||
event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow;
|
||||
|
||||
/* ----------------------------- Return response ---------------------------- */
|
||||
const response = await resolve(event);
|
||||
return response;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { inject, injectable } from 'tsyringe';
|
||||
import { ControllerProvider } from '../providers';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { registerEmailDto } from '../dtos/register-email.dto';
|
||||
import { IamService } from '../services/iam.service';
|
||||
|
|
@ -9,93 +8,92 @@ import { LuciaProvider } from '../providers/lucia.provider';
|
|||
import { requireAuth } from '../middleware/require-auth.middleware';
|
||||
import { updateEmailDto } from '../dtos/update-email.dto';
|
||||
import { verifyEmailDto } from '../dtos/verify-email.dto';
|
||||
import { Hono } from 'hono';
|
||||
import type { HonoTypes } from '../types';
|
||||
import type { Controller } from '../interfaces/controller.interface';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Controller */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ---------------------------------- About --------------------------------- */
|
||||
/*
|
||||
Controllers are responsible for handling incoming requests and returning responses
|
||||
to a client.
|
||||
*/
|
||||
/* ---------------------------------- Notes --------------------------------- */
|
||||
/*
|
||||
A controller should generally only handle routing and authorization through
|
||||
middleware.
|
||||
|
||||
Any business logic should be delegated to a service. This keeps the controller
|
||||
clean and easy to read.
|
||||
*/
|
||||
/* -------------------------------- Important ------------------------------- */
|
||||
/*
|
||||
Remember to register your controller in the api/index.ts file.
|
||||
*/
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
@injectable()
|
||||
export class IamController {
|
||||
export class IamController implements Controller {
|
||||
controller = new Hono<HonoTypes>();
|
||||
|
||||
constructor(
|
||||
@inject(ControllerProvider) private controller: ControllerProvider,
|
||||
@inject(IamService) private iamService: IamService,
|
||||
@inject(LuciaProvider) private lucia: LuciaProvider
|
||||
) {}
|
||||
|
||||
getAuthedUser() {
|
||||
return this.controller.get('/user', async (c) => {
|
||||
const user = c.var.user;
|
||||
return c.json({ user: user });
|
||||
});
|
||||
}
|
||||
|
||||
registerEmail() {
|
||||
return this.controller.post(
|
||||
'/email/register',
|
||||
zValidator('json', registerEmailDto),
|
||||
async (c) => {
|
||||
routes() {
|
||||
return this.controller
|
||||
.get('/user', async (c) => {
|
||||
const user = c.var.user;
|
||||
return c.json({ user: user });
|
||||
})
|
||||
.post('/email/register', zValidator('json', registerEmailDto), async (c) => {
|
||||
const { email } = c.req.valid('json');
|
||||
await this.iamService.registerEmail({ email });
|
||||
return c.json({ message: 'Verification email sent' });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
signInEmail() {
|
||||
return this.controller.post('/email/signin', zValidator('json', signInEmailDto), async (c) => {
|
||||
const { email, token } = c.req.valid('json');
|
||||
const session = await this.iamService.signinEmail({ email, token });
|
||||
const sessionCookie = this.lucia.createSessionCookie(session.id);
|
||||
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||
path: sessionCookie.attributes.path,
|
||||
maxAge: sessionCookie.attributes.maxAge,
|
||||
domain: sessionCookie.attributes.domain,
|
||||
sameSite: sessionCookie.attributes.sameSite as any,
|
||||
secure: sessionCookie.attributes.secure,
|
||||
httpOnly: sessionCookie.attributes.httpOnly,
|
||||
expires: sessionCookie.attributes.expires
|
||||
});
|
||||
return c.json({ message: 'ok' });
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
return this.controller.post('/logout', requireAuth, async (c) => {
|
||||
const sessionId = c.var.session.id;
|
||||
await this.iamService.logout(sessionId);
|
||||
const sessionCookie = this.lucia.createBlankSessionCookie();
|
||||
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||
path: sessionCookie.attributes.path,
|
||||
maxAge: sessionCookie.attributes.maxAge,
|
||||
domain: sessionCookie.attributes.domain,
|
||||
sameSite: sessionCookie.attributes.sameSite as any,
|
||||
secure: sessionCookie.attributes.secure,
|
||||
httpOnly: sessionCookie.attributes.httpOnly,
|
||||
expires: sessionCookie.attributes.expires
|
||||
});
|
||||
return c.json({ status: 'success' });
|
||||
});
|
||||
}
|
||||
|
||||
updateEmail() {
|
||||
return this.controller.post(
|
||||
'/email/update',
|
||||
requireAuth,
|
||||
zValidator('json', updateEmailDto),
|
||||
async (c) => {
|
||||
})
|
||||
.post('/email/signin', zValidator('json', signInEmailDto), async (c) => {
|
||||
const { email, token } = c.req.valid('json');
|
||||
const session = await this.iamService.signinEmail({ email, token });
|
||||
const sessionCookie = this.lucia.createSessionCookie(session.id);
|
||||
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||
path: sessionCookie.attributes.path,
|
||||
maxAge: sessionCookie.attributes.maxAge,
|
||||
domain: sessionCookie.attributes.domain,
|
||||
sameSite: sessionCookie.attributes.sameSite as any,
|
||||
secure: sessionCookie.attributes.secure,
|
||||
httpOnly: sessionCookie.attributes.httpOnly,
|
||||
expires: sessionCookie.attributes.expires
|
||||
});
|
||||
return c.json({ message: 'ok' });
|
||||
})
|
||||
.post('/logout', requireAuth, async (c) => {
|
||||
const sessionId = c.var.session.id;
|
||||
await this.iamService.logout(sessionId);
|
||||
const sessionCookie = this.lucia.createBlankSessionCookie();
|
||||
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||
path: sessionCookie.attributes.path,
|
||||
maxAge: sessionCookie.attributes.maxAge,
|
||||
domain: sessionCookie.attributes.domain,
|
||||
sameSite: sessionCookie.attributes.sameSite as any,
|
||||
secure: sessionCookie.attributes.secure,
|
||||
httpOnly: sessionCookie.attributes.httpOnly,
|
||||
expires: sessionCookie.attributes.expires
|
||||
});
|
||||
return c.json({ status: 'success' });
|
||||
})
|
||||
.post('/email/update', requireAuth, zValidator('json', updateEmailDto), async (c) => {
|
||||
const json = c.req.valid('json');
|
||||
await this.iamService.updateEmail(c.var.user.id, json);
|
||||
return c.json({ message: 'Verification email sent' });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
verifyEmail() {
|
||||
return this.controller.post(
|
||||
'/email/verify',
|
||||
requireAuth,
|
||||
zValidator('json', verifyEmailDto),
|
||||
async (c) => {
|
||||
})
|
||||
.post('/email/verify', requireAuth, zValidator('json', verifyEmailDto), async (c) => {
|
||||
const json = c.req.valid('json');
|
||||
await this.iamService.verifyEmail(c.var.user.id, json.token);
|
||||
return c.json({ message: 'Verified and updated' });
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,23 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* DTO */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ---------------------------------- About --------------------------------- */
|
||||
/*
|
||||
Data Transfer Objects (DTOs) are used to define the shape of data that is passed.
|
||||
They are used to validate data and ensure that the correct data is being passed
|
||||
to the correct methods.
|
||||
*/
|
||||
/* ---------------------------------- Notes --------------------------------- */
|
||||
/*
|
||||
DTO's are pretty flexible. You can use them anywhere you want in this application to
|
||||
validate or shape data. They are especially useful in API routes and services to
|
||||
ensure that the correct data is being passed around.
|
||||
*/
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
export const registerEmailDto = z.object({
|
||||
email: z.string().email()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,23 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* DTO */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ---------------------------------- About --------------------------------- */
|
||||
/*
|
||||
Data Transfer Objects (DTOs) are used to define the shape of data that is passed.
|
||||
They are used to validate data and ensure that the correct data is being passed
|
||||
to the correct methods.
|
||||
*/
|
||||
/* ---------------------------------- Notes --------------------------------- */
|
||||
/*
|
||||
DTO's are pretty flexible. You can use them anywhere you want in this application to
|
||||
validate or shape data. They are especially useful in API routes and services to
|
||||
ensure that the correct data is being passed around.
|
||||
*/
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
export const signInEmailDto = z.object({
|
||||
email: z.string().email(),
|
||||
token: z.string()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,23 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* DTO */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ---------------------------------- About --------------------------------- */
|
||||
/*
|
||||
Data Transfer Objects (DTOs) are used to define the shape of data that is passed.
|
||||
They are used to validate data and ensure that the correct data is being passed
|
||||
to the correct methods.
|
||||
*/
|
||||
/* ---------------------------------- Notes --------------------------------- */
|
||||
/*
|
||||
DTO's are pretty flexible. You can use them anywhere you want in this application to
|
||||
validate or shape data. They are especially useful in API routes and services to
|
||||
ensure that the correct data is being passed around.
|
||||
*/
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
export const updateEmailDto = z.object({
|
||||
email: z.string()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,23 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* DTO */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ---------------------------------- About --------------------------------- */
|
||||
/*
|
||||
Data Transfer Objects (DTOs) are used to define the shape of data that is passed.
|
||||
They are used to validate data and ensure that the correct data is being passed
|
||||
to the correct methods.
|
||||
*/
|
||||
/* ---------------------------------- Notes --------------------------------- */
|
||||
/*
|
||||
DTO's are pretty flexible. You can use them anywhere you want in this application to
|
||||
validate or shape data. They are especially useful in API routes and services to
|
||||
ensure that the correct data is being passed around.
|
||||
*/
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
export const verifyEmailDto = z.object({
|
||||
token: z.string()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,30 +5,45 @@ import { hc } from 'hono/client';
|
|||
import { container } from 'tsyringe';
|
||||
import { processAuth } from './middleware/process-auth.middleware';
|
||||
import { IamController } from './controllers/iam.controller';
|
||||
import { config } from './common/config';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* API */
|
||||
/* Client Request */
|
||||
/* ------------------------------------ ▲ ----------------------------------- */
|
||||
/* ------------------------------------ | ----------------------------------- */
|
||||
/* ------------------------------------ ▼ ----------------------------------- */
|
||||
/* Controller */
|
||||
/* ---------------------------- (Request Routing) --------------------------- */
|
||||
/* ------------------------------------ ▲ ----------------------------------- */
|
||||
/* ------------------------------------ | ----------------------------------- */
|
||||
/* ------------------------------------ ▼ ----------------------------------- */
|
||||
/* Service */
|
||||
/* ---------------------------- (Business logic) ---------------------------- */
|
||||
/* ------------------------------------ ▲ ----------------------------------- */
|
||||
/* ------------------------------------ | ----------------------------------- */
|
||||
/* ------------------------------------ ▼ ----------------------------------- */
|
||||
/* Repository */
|
||||
/* ----------------------------- (Data storage) ----------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/* ----------------------------------- Api ---------------------------------- */
|
||||
const app = new Hono().basePath('/api');
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Global Middlewares */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
app.use(processAuth); // all this does is set the session and user variables in the request object
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Routes */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* --------------------------- Global Middlewares --------------------------- */
|
||||
app.use(processAuth);
|
||||
|
||||
/* --------------------------------- Routes --------------------------------- */
|
||||
const routes = app
|
||||
.route('/iam', container.resolve(IamController).registerEmail())
|
||||
.route('/iam', container.resolve(IamController).signInEmail())
|
||||
.route('/iam', container.resolve(IamController).getAuthedUser())
|
||||
.route('/iam', container.resolve(IamController).updateEmail())
|
||||
.route('/iam', container.resolve(IamController).verifyEmail());
|
||||
.route('/iam', container.resolve(IamController).routes())
|
||||
.route('/iam', container.resolve(IamController).routes())
|
||||
.route('/iam', container.resolve(IamController).routes())
|
||||
.route('/iam', container.resolve(IamController).routes())
|
||||
.route('/iam', container.resolve(IamController).routes());
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Exports */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
export const rpc = hc<typeof routes>('http://localhost:5173');
|
||||
export const rpc = hc<typeof routes>(config.ORIGIN);
|
||||
export type ApiClient = typeof rpc;
|
||||
export type ApiRoutes = typeof routes;
|
||||
export { app };
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
<html lang='en'>
|
||||
<head>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||
<title>Message</title>
|
||||
</head>
|
||||
<body>
|
||||
<p class='title'>Welcome to Example</p>
|
||||
<p>
|
||||
Thanks for using example.com. We want to make sure it's really you. Please enter the following
|
||||
verification code when prompted. If you don't have an exmaple.com an account, you can ignore
|
||||
this message.</p>
|
||||
</body>
|
||||
<style>
|
||||
.title { font-size: 24px; font-weight: 700; } .token-text { font-size: 24px; font-weight: 700; margin-top: 8px; }
|
||||
.token-title { font-size: 18px; font-weight: 700; margin-bottom: 0px; }
|
||||
.center { display: flex; justify-content: center; align-items: center; flex-direction: column;}
|
||||
.token-subtext { font-size: 12px; margin-top: 0px; }
|
||||
</style>
|
||||
</html>
|
||||
8
src/lib/server/api/interfaces/controller.interface.ts
Normal file
8
src/lib/server/api/interfaces/controller.interface.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Hono } from 'hono';
|
||||
import type { HonoTypes } from '../types';
|
||||
import type { BlankSchema } from 'hono/types';
|
||||
|
||||
export interface Controller {
|
||||
controller: Hono<HonoTypes, BlankSchema, '/'>;
|
||||
routes(): any;
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { container } from 'tsyringe';
|
||||
import { Hono } from 'hono';
|
||||
import type { HonoTypes } from '../types';
|
||||
|
||||
// Symbol
|
||||
export const ControllerProvider = Symbol('CONTROLLER_PROVIDER');
|
||||
|
||||
// Type
|
||||
export type ControllerProvider = typeof controller;
|
||||
|
||||
// Value
|
||||
const controller = new Hono<HonoTypes>();
|
||||
|
||||
// Register
|
||||
container.register<ControllerProvider>(ControllerProvider, { useValue: controller });
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
export * from './database.provider';
|
||||
export * from './controller.provider'
|
||||
export * from './lucia.provider';
|
||||
|
|
|
|||
|
|
@ -5,14 +5,24 @@ import { tokensTable } from '../infrastructure/database/tables';
|
|||
import { takeFirstOrThrow } from '../infrastructure/database/utils';
|
||||
import type { Repository } from '../interfaces/repository.interface';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Types */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
export type InsertToken = InferInsertModel<typeof tokensTable>;
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* 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 InsertToken = InferInsertModel<typeof tokensTable>;
|
||||
|
||||
@injectable()
|
||||
export class TokensRepository implements Repository {
|
||||
constructor(@inject(DatabaseProvider) private db: DatabaseProvider) {}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,29 @@
|
|||
import { inject, injectable } from 'tsyringe';
|
||||
import type { Repository } from '../interfaces/repository.interface';
|
||||
import { DatabaseProvider, type DatabaseProvider } from '../providers';
|
||||
import { DatabaseProvider } from '../providers';
|
||||
import { eq, type InferInsertModel } from 'drizzle-orm';
|
||||
import { usersTable } from '../infrastructure/database/tables/users.table';
|
||||
import { takeFirstOrThrow } from '../infrastructure/database/utils';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Types */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
export type CreateUser = Pick<InferInsertModel<typeof usersTable>, 'avatar' | 'email' | 'verified'>;
|
||||
export type UpdateUser = Partial<CreateUser>;
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* 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>;
|
||||
|
||||
@injectable()
|
||||
export class UsersRepository implements Repository {
|
||||
constructor(@inject(DatabaseProvider) private db: DatabaseProvider) {}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,24 @@ import type { SignInEmailDto } from '../dtos/signin-email.dto';
|
|||
import { BadRequest } from '../common/errors';
|
||||
import { LuciaProvider } from '../providers/lucia.provider';
|
||||
import type { UpdateEmailDto } from '../dtos/update-email.dto';
|
||||
import type { VerifyEmailDto } from '../dtos/verify-email.dto';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* 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.
|
||||
*/
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
@injectable()
|
||||
export class IamService {
|
||||
|
|
@ -45,10 +62,9 @@ export class IamService {
|
|||
// if this is a new unverified user, send a welcome email and update the user
|
||||
if (!user.verified) {
|
||||
await this.usersRepository.update(user.id, { verified: true });
|
||||
await this.mailerService.send({
|
||||
this.mailerService.sendWelcomeEmail({
|
||||
to: user.email,
|
||||
subject: 'Welcome!',
|
||||
html: 'Welcome to example.com'
|
||||
props: null
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -81,14 +97,9 @@ export class IamService {
|
|||
|
||||
private async createValidationReuqest(userId: string, email: string) {
|
||||
const validationToken = await this.tokensService.create(userId, email);
|
||||
await this.mailerService.sendEmailVerification({
|
||||
this.mailerService.sendEmailVerification({
|
||||
to: email,
|
||||
props: { token: validationToken.token }
|
||||
});
|
||||
// return await this.mailerService.send({
|
||||
// to: email,
|
||||
// subject: 'Verify your email',
|
||||
// html: `Your token is ${validationToken.token}`
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,24 @@ import path from 'path';
|
|||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* 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;
|
||||
|
|
@ -37,6 +55,15 @@ export class MailerService {
|
|||
});
|
||||
}
|
||||
|
||||
sendWelcomeEmail(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
|
||||
|
|
|
|||
|
|
@ -4,6 +4,24 @@ import dayjs from 'dayjs';
|
|||
import { customAlphabet } from 'nanoid';
|
||||
import { DatabaseProvider } from '../providers';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* 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.
|
||||
*/
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
@injectable()
|
||||
export class TokensService {
|
||||
constructor(
|
||||
|
|
|
|||
15
src/lib/types.ts
Normal file
15
src/lib/types.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export type SwapDatesWithStrings<T> = {
|
||||
[k in keyof T]: T[k] extends Date | undefined
|
||||
? string
|
||||
: T[k] extends object
|
||||
? SwapDatesWithStrings<T[k]>
|
||||
: T[k];
|
||||
};
|
||||
|
||||
export type Returned<T> = {
|
||||
[k in keyof T]: T[k] extends Date | undefined
|
||||
? string
|
||||
: T[k] extends object
|
||||
? SwapDatesWithStrings<T[k]>
|
||||
: T[k];
|
||||
};
|
||||
|
|
@ -1,14 +1,8 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import ViteRestart from 'vite-plugin-restart';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
ViteRestart({
|
||||
restart: ['./src/lib/server/api/**']
|
||||
})
|
||||
],
|
||||
plugins: [sveltekit()],
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue