Removed fragmented controllers in favor of hono's native chaining

This commit is contained in:
rykuno 2024-05-27 12:38:59 -05:00
parent 163b8bdc52
commit 653e2c22cf
20 changed files with 354 additions and 149 deletions

View file

@ -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. 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); 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. 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.
@ -50,18 +67,20 @@ My selection of libraries are just what I've found success, **stable** and high
## Architecture ## Architecture
We have a few popular architectures for structuring backends We have a few popular architectures for structuring traditional backends
* 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 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
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. 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 `authentication.service.test.ts` or `users.service.test.ts`
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
View file

@ -8,8 +8,8 @@ declare global {
// interface Error {} // interface Error {}
interface Locals { interface Locals {
api: ApiClient['api']; api: ApiClient['api'];
getAuthedUser: () => Promise<User | null>; getAuthedUser: () => Promise<Returned<User> | null>;
getAuthedUserOrThrow: () => Promise<User>; getAuthedUserOrThrow: () => Promise<Returned<User>>;
} }
// interface PageData {} // interface PageData {}
// interface PageState {} // interface PageState {}

View file

@ -1,10 +1,11 @@
import { hc, type ClientResponse } from 'hono/client'; import { hc } from 'hono/client';
import { redirect, type Handle } from '@sveltejs/kit'; import { redirect, type Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks'; import { sequence } from '@sveltejs/kit/hooks';
import type { ApiRoutes } from '$lib/server/api'; import type { ApiRoutes } from '$lib/server/api';
import { parseApiResponse } from '$lib/helpers'; import { parseApiResponse } from '$lib/helpers';
const apiClient: Handle = async ({ event, resolve }) => { const apiClient: Handle = async ({ event, resolve }) => {
/* ------------------------------ Register api ------------------------------ */
const { api } = hc<ApiRoutes>('/', { const { api } = hc<ApiRoutes>('/', {
fetch: event.fetch, fetch: event.fetch,
headers: { headers: {
@ -12,6 +13,7 @@ const apiClient: Handle = async ({ event, resolve }) => {
} }
}); });
/* ----------------------------- Auth functions ----------------------------- */
async function getAuthedUser() { async function getAuthedUser() {
const { data } = await parseApiResponse(api.iam.user.$get()); const { data } = await parseApiResponse(api.iam.user.$get());
return data && data.user; return data && data.user;
@ -23,11 +25,12 @@ const apiClient: Handle = async ({ event, resolve }) => {
return data?.user; return data?.user;
} }
// set contexts /* ------------------------------ Set contexts ------------------------------ */
event.locals.api = api; event.locals.api = api;
event.locals.getAuthedUser = getAuthedUser; event.locals.getAuthedUser = getAuthedUser;
event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow; event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow;
/* ----------------------------- Return response ---------------------------- */
const response = await resolve(event); const response = await resolve(event);
return response; return response;
}; };

View file

@ -1,5 +1,4 @@
import { inject, injectable } from 'tsyringe'; import { inject, injectable } from 'tsyringe';
import { ControllerProvider } from '../providers';
import { zValidator } from '@hono/zod-validator'; import { zValidator } from '@hono/zod-validator';
import { registerEmailDto } from '../dtos/register-email.dto'; import { registerEmailDto } from '../dtos/register-email.dto';
import { IamService } from '../services/iam.service'; import { IamService } from '../services/iam.service';
@ -9,36 +8,54 @@ import { LuciaProvider } from '../providers/lucia.provider';
import { requireAuth } from '../middleware/require-auth.middleware'; import { requireAuth } from '../middleware/require-auth.middleware';
import { updateEmailDto } from '../dtos/update-email.dto'; import { updateEmailDto } from '../dtos/update-email.dto';
import { verifyEmailDto } from '../dtos/verify-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() @injectable()
export class IamController { export class IamController implements Controller {
controller = new Hono<HonoTypes>();
constructor( constructor(
@inject(ControllerProvider) private controller: ControllerProvider,
@inject(IamService) private iamService: IamService, @inject(IamService) private iamService: IamService,
@inject(LuciaProvider) private lucia: LuciaProvider @inject(LuciaProvider) private lucia: LuciaProvider
) {} ) {}
getAuthedUser() { routes() {
return this.controller.get('/user', async (c) => { return this.controller
.get('/user', async (c) => {
const user = c.var.user; const user = c.var.user;
return c.json({ user: user }); return c.json({ user: user });
}); })
} .post('/email/register', zValidator('json', registerEmailDto), async (c) => {
registerEmail() {
return this.controller.post(
'/email/register',
zValidator('json', registerEmailDto),
async (c) => {
const { email } = c.req.valid('json'); const { email } = c.req.valid('json');
await this.iamService.registerEmail({ email }); await this.iamService.registerEmail({ email });
return c.json({ message: 'Verification email sent' }); return c.json({ message: 'Verification email sent' });
} })
); .post('/email/signin', zValidator('json', signInEmailDto), async (c) => {
}
signInEmail() {
return this.controller.post('/email/signin', zValidator('json', signInEmailDto), async (c) => {
const { email, token } = c.req.valid('json'); const { email, token } = c.req.valid('json');
const session = await this.iamService.signinEmail({ email, token }); const session = await this.iamService.signinEmail({ email, token });
const sessionCookie = this.lucia.createSessionCookie(session.id); const sessionCookie = this.lucia.createSessionCookie(session.id);
@ -52,11 +69,8 @@ export class IamController {
expires: sessionCookie.attributes.expires expires: sessionCookie.attributes.expires
}); });
return c.json({ message: 'ok' }); return c.json({ message: 'ok' });
}); })
} .post('/logout', requireAuth, async (c) => {
logout() {
return this.controller.post('/logout', requireAuth, async (c) => {
const sessionId = c.var.session.id; const sessionId = c.var.session.id;
await this.iamService.logout(sessionId); await this.iamService.logout(sessionId);
const sessionCookie = this.lucia.createBlankSessionCookie(); const sessionCookie = this.lucia.createBlankSessionCookie();
@ -70,32 +84,16 @@ export class IamController {
expires: sessionCookie.attributes.expires expires: sessionCookie.attributes.expires
}); });
return c.json({ status: 'success' }); return c.json({ status: 'success' });
}); })
} .post('/email/update', requireAuth, zValidator('json', updateEmailDto), async (c) => {
updateEmail() {
return this.controller.post(
'/email/update',
requireAuth,
zValidator('json', updateEmailDto),
async (c) => {
const json = c.req.valid('json'); const json = c.req.valid('json');
await this.iamService.updateEmail(c.var.user.id, json); await this.iamService.updateEmail(c.var.user.id, json);
return c.json({ message: 'Verification email sent' }); return c.json({ message: 'Verification email sent' });
} })
); .post('/email/verify', requireAuth, zValidator('json', verifyEmailDto), async (c) => {
}
verifyEmail() {
return this.controller.post(
'/email/verify',
requireAuth,
zValidator('json', verifyEmailDto),
async (c) => {
const json = c.req.valid('json'); const json = c.req.valid('json');
await this.iamService.verifyEmail(c.var.user.id, json.token); await this.iamService.verifyEmail(c.var.user.id, json.token);
return c.json({ message: 'Verified and updated' }); return c.json({ message: 'Verified and updated' });
} });
);
} }
} }

View file

@ -1,5 +1,23 @@
import { z } from 'zod'; 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({ export const registerEmailDto = z.object({
email: z.string().email() email: z.string().email()
}); });

View file

@ -1,5 +1,23 @@
import { z } from 'zod'; 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({ export const signInEmailDto = z.object({
email: z.string().email(), email: z.string().email(),
token: z.string() token: z.string()

View file

@ -1,5 +1,23 @@
import { z } from 'zod'; 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({ export const updateEmailDto = z.object({
email: z.string() email: z.string()
}); });

View file

@ -1,5 +1,23 @@
import { z } from 'zod'; 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({ export const verifyEmailDto = z.object({
token: z.string() token: z.string()
}); });

View file

@ -5,30 +5,45 @@ import { hc } from 'hono/client';
import { container } from 'tsyringe'; import { container } from 'tsyringe';
import { processAuth } from './middleware/process-auth.middleware'; import { processAuth } from './middleware/process-auth.middleware';
import { IamController } from './controllers/iam.controller'; 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'); const app = new Hono().basePath('/api');
/* -------------------------------------------------------------------------- */ /* --------------------------- Global Middlewares --------------------------- */
/* Global Middlewares */ app.use(processAuth);
/* -------------------------------------------------------------------------- */
app.use(processAuth); // all this does is set the session and user variables in the request object /* --------------------------------- Routes --------------------------------- */
/* -------------------------------------------------------------------------- */
/* Routes */
/* -------------------------------------------------------------------------- */
const routes = app const routes = app
.route('/iam', container.resolve(IamController).registerEmail()) .route('/iam', container.resolve(IamController).routes())
.route('/iam', container.resolve(IamController).signInEmail()) .route('/iam', container.resolve(IamController).routes())
.route('/iam', container.resolve(IamController).getAuthedUser()) .route('/iam', container.resolve(IamController).routes())
.route('/iam', container.resolve(IamController).updateEmail()) .route('/iam', container.resolve(IamController).routes())
.route('/iam', container.resolve(IamController).verifyEmail()); .route('/iam', container.resolve(IamController).routes());
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Exports */ /* 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 ApiClient = typeof rpc;
export type ApiRoutes = typeof routes; export type ApiRoutes = typeof routes;
export { app }; export { app };

View file

@ -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>

View 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;
}

View file

@ -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 });

View file

@ -1,2 +1,2 @@
export * from './database.provider'; export * from './database.provider';
export * from './controller.provider' export * from './lucia.provider';

View file

@ -5,14 +5,24 @@ import { tokensTable } from '../infrastructure/database/tables';
import { takeFirstOrThrow } from '../infrastructure/database/utils'; import { takeFirstOrThrow } from '../infrastructure/database/utils';
import type { Repository } from '../interfaces/repository.interface'; import type { Repository } from '../interfaces/repository.interface';
/* -------------------------------------------------------------------------- */
/* Types */
/* -------------------------------------------------------------------------- */
export type InsertToken = InferInsertModel<typeof tokensTable>;
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Repository */ /* 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() @injectable()
export class TokensRepository implements Repository { export class TokensRepository implements Repository {
constructor(@inject(DatabaseProvider) private db: DatabaseProvider) {} constructor(@inject(DatabaseProvider) private db: DatabaseProvider) {}

View file

@ -1,19 +1,29 @@
import { inject, injectable } from 'tsyringe'; import { inject, injectable } from 'tsyringe';
import type { Repository } from '../interfaces/repository.interface'; 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 { eq, type InferInsertModel } from 'drizzle-orm';
import { usersTable } from '../infrastructure/database/tables/users.table'; import { usersTable } from '../infrastructure/database/tables/users.table';
import { takeFirstOrThrow } from '../infrastructure/database/utils'; import { takeFirstOrThrow } from '../infrastructure/database/utils';
/* -------------------------------------------------------------------------- */
/* Types */
/* -------------------------------------------------------------------------- */
export type CreateUser = Pick<InferInsertModel<typeof usersTable>, 'avatar' | 'email' | 'verified'>;
export type UpdateUser = Partial<CreateUser>;
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Repository */ /* 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() @injectable()
export class UsersRepository implements Repository { export class UsersRepository implements Repository {
constructor(@inject(DatabaseProvider) private db: DatabaseProvider) {} constructor(@inject(DatabaseProvider) private db: DatabaseProvider) {}

View file

@ -7,7 +7,24 @@ import type { SignInEmailDto } from '../dtos/signin-email.dto';
import { BadRequest } from '../common/errors'; import { BadRequest } from '../common/errors';
import { LuciaProvider } from '../providers/lucia.provider'; import { LuciaProvider } from '../providers/lucia.provider';
import type { UpdateEmailDto } from '../dtos/update-email.dto'; 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() @injectable()
export class IamService { 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 this is a new unverified user, send a welcome email and update the user
if (!user.verified) { if (!user.verified) {
await this.usersRepository.update(user.id, { verified: true }); await this.usersRepository.update(user.id, { verified: true });
await this.mailerService.send({ this.mailerService.sendWelcomeEmail({
to: user.email, to: user.email,
subject: 'Welcome!', props: null
html: 'Welcome to example.com'
}); });
} }
@ -81,14 +97,9 @@ export class IamService {
private async createValidationReuqest(userId: string, email: string) { private async createValidationReuqest(userId: string, email: string) {
const validationToken = await this.tokensService.create(userId, email); const validationToken = await this.tokensService.create(userId, email);
await this.mailerService.sendEmailVerification({ this.mailerService.sendEmailVerification({
to: email, to: email,
props: { token: validationToken.token } props: { token: validationToken.token }
}); });
// return await this.mailerService.send({
// to: email,
// subject: 'Verify your email',
// html: `Your token is ${validationToken.token}`
// });
} }
} }

View file

@ -5,6 +5,24 @@ import path from 'path';
import fs from 'fs'; import fs from 'fs';
import { fileURLToPath } from 'url'; 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 = { type SendMail = {
to: string | string[]; to: string | string[];
subject: 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) { private async send({ to, subject, html }: SendMail) {
const message = await this.nodemailer.sendMail({ const message = await this.nodemailer.sendMail({
from: '"Example" <example@ethereal.email>', // sender address from: '"Example" <example@ethereal.email>', // sender address

View file

@ -4,6 +4,24 @@ import dayjs from 'dayjs';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
import { DatabaseProvider } from '../providers'; 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() @injectable()
export class TokensService { export class TokensService {
constructor( constructor(

15
src/lib/types.ts Normal file
View 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];
};

View file

@ -1,14 +1,8 @@
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
import ViteRestart from 'vite-plugin-restart';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [sveltekit()],
sveltekit(),
ViteRestart({
restart: ['./src/lib/server/api/**']
})
],
test: { test: {
include: ['src/**/*.{test,spec}.{js,ts}'] include: ['src/**/*.{test,spec}.{js,ts}']
} }