Merge branch 'feature/project-restructure'

This commit is contained in:
rykuno 2024-09-01 23:36:58 -05:00
commit 53ebe9a157
68 changed files with 575 additions and 375 deletions

View file

@ -1,5 +1,15 @@
# API
ORIGIN=http://localhost:5173 ORIGIN=http://localhost:5173
# Database # Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
REDIS_URL=redis://localhost:6379
# Redis
REDIS_URL=redis://localhost:6379
# Storage
PUBLIC_IMAGE_URI=http://localhost:9000/dev
STORAGE_BUCKET=dev
STORAGE_URL=http://localhost:9000
STORAGE_ACCESS_KEY=user
STORAGE_SECRET_KEY=password

3
Dockerfile.minio Normal file
View file

@ -0,0 +1,3 @@
FROM nginx:stable-alpine-slim
COPY minio-console.conf.template /etc/nginx/templates/
RUN rm /etc/nginx/conf.d/default.conf /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh

View file

@ -1,8 +1,8 @@
import type { Config } from 'drizzle-kit'; import type { Config } from 'drizzle-kit';
export default { export default {
out: './src/lib/server/api/databases/migrations', out: './src/lib/server/api/databases/postgres/migrations',
schema: './src/lib/server/api/databases/tables/*.table.ts', schema: './src/lib/server/api/databases/postgres/tables/*.table.ts',
breakpoints: false, breakpoints: false,
strict: true, strict: true,
dialect: 'postgresql', dialect: 'postgresql',

View file

@ -0,0 +1,41 @@
upstream minio_console {
server ${MINIO_HOST}:${MINIO_CONSOLE_PORT};
}
server {
listen ${PORT};
# Allow special characters in headers
ignore_invalid_headers off;
# Allow any size file to be uploaded.
# Set to a value such as 1000m; to restrict file size to a specific value
client_max_body_size 0;
# Disable buffering
proxy_buffering off;
proxy_request_buffering off;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
# This is necessary to pass the correct IP to be hashed
real_ip_header X-Real-IP;
proxy_connect_timeout 300;
# To support websockets in MinIO versions released after January 2023
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Some environments may encounter CORS errors (Kubernetes + Nginx Ingress)
# Uncomment the following line to set the Origin request to an empty string
# proxy_set_header Origin '';
chunked_transfer_encoding off;
proxy_pass http://minio_console; # This uses the upstream directive definition to load balance
}
}

View file

@ -87,6 +87,8 @@
"mode-watcher": "^0.4.1", "mode-watcher": "^0.4.1",
"paneforge": "^0.0.5", "paneforge": "^0.0.5",
"rate-limit-redis": "^4.2.0", "rate-limit-redis": "^4.2.0",
"redis": "^4.7.0",
"redis-om": "^0.4.6",
"resend": "^3.5.0", "resend": "^3.5.0",
"svelte-sonner": "^0.3.27", "svelte-sonner": "^0.3.27",
"tailwind-merge": "^2.4.0", "tailwind-merge": "^2.4.0",

View file

@ -38,6 +38,12 @@ importers:
rate-limit-redis: rate-limit-redis:
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.2.0(express-rate-limit@7.4.0(express@4.19.2)) version: 4.2.0(express-rate-limit@7.4.0(express@4.19.2))
redis:
specifier: ^4.7.0
version: 4.7.0
redis-om:
specifier: ^0.4.6
version: 0.4.6
resend: resend:
specifier: ^3.5.0 specifier: ^3.5.0
version: 3.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 3.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -1416,6 +1422,35 @@ packages:
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
'@redis/bloom@1.2.0':
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/client@1.6.0':
resolution: {integrity: sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==}
engines: {node: '>=14'}
'@redis/graph@1.1.1':
resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/json@1.0.7':
resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/search@1.2.0':
resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/time-series@1.1.0':
resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==}
peerDependencies:
'@redis/client': ^1.0.0
'@rollup/plugin-commonjs@26.0.1': '@rollup/plugin-commonjs@26.0.1':
resolution: {integrity: sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==} resolution: {integrity: sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==}
engines: {node: '>=16.0.0 || 14 >= 14.17'} engines: {node: '>=16.0.0 || 14 >= 14.17'}
@ -2638,6 +2673,10 @@ packages:
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
generic-pool@3.9.0:
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
engines: {node: '>= 4'}
get-func-name@2.0.2: get-func-name@2.0.2:
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
@ -2872,6 +2911,10 @@ packages:
json-stable-stringify-without-jsonify@1.0.1: json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
jsonpath-plus@7.2.0:
resolution: {integrity: sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==}
engines: {node: '>=12.0.0'}
just-clone@6.2.0: just-clone@6.2.0:
resolution: {integrity: sha512-1IynUYEc/HAwxhi3WDpIpxJbZpMCvvrrmZVqvj9EhpvbH8lls7HhdhiByjL7DkAaWlLIzpC0Xc/VPvy/UxLNjA==} resolution: {integrity: sha512-1IynUYEc/HAwxhi3WDpIpxJbZpMCvvrrmZVqvj9EhpvbH8lls7HhdhiByjL7DkAaWlLIzpC0Xc/VPvy/UxLNjA==}
@ -3494,10 +3537,17 @@ packages:
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
engines: {node: '>=4'} engines: {node: '>=4'}
redis-om@0.4.6:
resolution: {integrity: sha512-L6cfZfG+I7ES+hHfBBKQwUEbfmGQJhIvcreP5NgxkxX+LtYLLXrcpP/sIIW1jMyjdgJE1KRjAbiyiuL2AAHe3g==}
engines: {node: '>= 14'}
redis-parser@3.0.0: redis-parser@3.0.0:
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
engines: {node: '>=4'} engines: {node: '>=4'}
redis@4.7.0:
resolution: {integrity: sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==}
reflect-metadata@0.2.2: reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
@ -3883,6 +3933,10 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
ulid@2.3.0:
resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==}
hasBin: true
undici-types@6.13.0: undici-types@6.13.0:
resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==}
@ -4024,6 +4078,9 @@ packages:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yaml@1.10.2: yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -5252,6 +5309,32 @@ snapshots:
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
react-promise-suspense: 0.3.4 react-promise-suspense: 0.3.4
'@redis/bloom@1.2.0(@redis/client@1.6.0)':
dependencies:
'@redis/client': 1.6.0
'@redis/client@1.6.0':
dependencies:
cluster-key-slot: 1.1.2
generic-pool: 3.9.0
yallist: 4.0.0
'@redis/graph@1.1.1(@redis/client@1.6.0)':
dependencies:
'@redis/client': 1.6.0
'@redis/json@1.0.7(@redis/client@1.6.0)':
dependencies:
'@redis/client': 1.6.0
'@redis/search@1.2.0(@redis/client@1.6.0)':
dependencies:
'@redis/client': 1.6.0
'@redis/time-series@1.1.0(@redis/client@1.6.0)':
dependencies:
'@redis/client': 1.6.0
'@rollup/plugin-commonjs@26.0.1(rollup@4.20.0)': '@rollup/plugin-commonjs@26.0.1(rollup@4.20.0)':
dependencies: dependencies:
'@rollup/pluginutils': 5.1.0(rollup@4.20.0) '@rollup/pluginutils': 5.1.0(rollup@4.20.0)
@ -6690,6 +6773,8 @@ snapshots:
function-bind@1.1.2: {} function-bind@1.1.2: {}
generic-pool@3.9.0: {}
get-func-name@2.0.2: {} get-func-name@2.0.2: {}
get-intrinsic@1.2.4: get-intrinsic@1.2.4:
@ -6945,6 +7030,8 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {} json-stable-stringify-without-jsonify@1.0.1: {}
jsonpath-plus@7.2.0: {}
just-clone@6.2.0: {} just-clone@6.2.0: {}
keyv@4.5.4: keyv@4.5.4:
@ -7428,10 +7515,26 @@ snapshots:
redis-errors@1.2.0: {} redis-errors@1.2.0: {}
redis-om@0.4.6:
dependencies:
jsonpath-plus: 7.2.0
just-clone: 6.2.0
redis: 4.7.0
ulid: 2.3.0
redis-parser@3.0.0: redis-parser@3.0.0:
dependencies: dependencies:
redis-errors: 1.2.0 redis-errors: 1.2.0
redis@4.7.0:
dependencies:
'@redis/bloom': 1.2.0(@redis/client@1.6.0)
'@redis/client': 1.6.0
'@redis/graph': 1.1.1(@redis/client@1.6.0)
'@redis/json': 1.0.7(@redis/client@1.6.0)
'@redis/search': 1.2.0(@redis/client@1.6.0)
'@redis/time-series': 1.1.0(@redis/client@1.6.0)
reflect-metadata@0.2.2: {} reflect-metadata@0.2.2: {}
regenerator-runtime@0.14.1: regenerator-runtime@0.14.1:
@ -7870,6 +7973,8 @@ snapshots:
typescript@5.5.4: {} typescript@5.5.4: {}
ulid@2.3.0: {}
undici-types@6.13.0: {} undici-types@6.13.0: {}
unpipe@1.0.0: {} unpipe@1.0.0: {}
@ -7995,6 +8100,8 @@ snapshots:
xtend@4.0.2: {} xtend@4.0.2: {}
yallist@4.0.0: {}
yaml@1.10.2: {} yaml@1.10.2: {}
yaml@2.5.0: {} yaml@2.5.0: {}

View file

@ -1,4 +1,5 @@
services: services:
# Web
- type: web - type: web
name: web name: web
runtime: node runtime: node
@ -9,6 +10,21 @@ services:
startCommand: npm run db:migrate && node build/index.js startCommand: npm run db:migrate && node build/index.js
healthCheckPath: / healthCheckPath: /
envVars: envVars:
- key: STORAGE_API_SECRET_KEY
fromService:
name: minio-server
type: web
property: MINIO_ROOT_PASSWORD
- key: STORAGE_ACCESS_KEY
fromService:
name: minio-server
type: web
property: MINIO_ROOT_USER
- key: STORAGE_API_URL
fromService:
name: minio-server
type: web
property: connectionString
- key: DATABASE_URL - key: DATABASE_URL
fromDatabase: fromDatabase:
name: tofustack name: tofustack
@ -23,10 +39,62 @@ services:
name: web name: web
type: web type: web
property: host property: host
# Redis
- type: redis - type: redis
name: redis name: redis
ipAllowList: [] # Only allow internal connections ipAllowList: [] # Only allow internal connections
# MinIO Server
- type: web
name: minio-server
healthCheckPath: /minio/health/liveweb
runtime: image
image:
url: docker.io/minio/minio:latest
dockerCommand: minio server /data --address $HOST:$PORT --console-address $HOST:$CONSOLE_PORT
# Use the following 'dockerCommand' if removing the 'minio-console'
# web service
# dockerCommand: minio server /data --address $HOST:$PORT
autoDeploy: false
disk:
name: data
mountPath: /data
envVars:
- key: MINIO_ROOT_USER
generateValue: true
- key: MINIO_ROOT_PASSWORD
generateValue: true
- key: HOST
value: "0.0.0.0"
- key: PORT
value: 9000
- key: CONSOLE_PORT
value: 9090
# Uncomment the following key/value pair if you are removing the
# 'minio-console' web service
# - key: MINIO_BROWSER
# value: "off"
# MinIO Console
- type: web
name: minio-console
runtime: docker
dockerContext: /
dockerfilePath: ./Dockerfile.minio
autoDeploy: false
envVars:
- key: PORT
value: 10000
- key: MINIO_HOST
fromService:
name: minio-server
type: web
property: host
- key: MINIO_CONSOLE_PORT
fromService:
name: minio-server
type: web
envVarKey: CONSOLE_PORT
databases: databases:
- name: db - name: db
databaseName: tofustack databaseName: tofustack

View file

@ -1,25 +0,0 @@
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()
});
export type RegisterEmailDto = z.infer<typeof registerEmailDto>;

View file

@ -1,26 +0,0 @@
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()
});
export type SignInEmailDto = z.infer<typeof signInEmailDto>;

View file

@ -1,24 +0,0 @@
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().email()
});
export type UpdateEmailDto = z.infer<typeof updateEmailDto>;

View file

@ -1,24 +0,0 @@
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()
});
export type VerifyEmailDto = z.infer<typeof verifyEmailDto>;

View file

@ -0,0 +1,22 @@
import * as envs from '$env/static/private';
import type { Config } from './types/config.type';
export const config: Config = {
isProduction: envs.NODE_ENV === 'production',
api: {
origin: envs.ORIGIN,
},
storage: {
accessKey: envs.STORAGE_ACCESS_KEY,
secretKey: envs.STORAGE_SECRET_KEY,
bucket: envs.STORAGE_BUCKET,
url: envs.STORAGE_URL
},
postgres: {
url: envs.DATABASE_URL
},
redis: {
url: envs.REDIS_URL
}
}

View file

@ -1,7 +0,0 @@
import { Hono } from 'hono';
import type { BlankSchema } from 'hono/types';
import type { HonoTypes } from '../types/hono.type';
// export interface Controller {
// routes()
// }

View file

@ -1,5 +0,0 @@
import type { DatabaseProvider } from "../../providers/database.provider";
export interface Repository {
trxHost(trx: DatabaseProvider): any;
}

View file

@ -0,0 +1,3 @@
export abstract class AsyncService {
async init(): Promise<void> { }
}

View file

@ -0,0 +1,26 @@
export interface Config {
isProduction: boolean;
api: ApiConfig;
storage: StorageConfig;
redis: RedisConfig;
postgres: PostgresConfig;
}
interface ApiConfig {
origin: string;
}
interface StorageConfig {
accessKey: string;
secretKey: string;
bucket: string;
url: string;
}
interface RedisConfig {
url: string;
}
interface PostgresConfig {
url: string;
}

View file

@ -1,6 +1,6 @@
import { Hono } from "hono"; import { Hono } from "hono";
import type { HonoTypes } from "../types/hono.type"; import type { HonoTypes } from "./hono";
import type { BlankSchema, Env, Schema } from "hono/types"; import type { BlankSchema } from "hono/types";
export abstract class Controler { export abstract class Controler {
protected readonly controller: Hono<HonoTypes, BlankSchema, '/'>; protected readonly controller: Hono<HonoTypes, BlankSchema, '/'>;

View file

@ -1,6 +1,3 @@
export interface Email { export interface Email {
subject(): string subject(): string
html(): string; html(): string;

View file

@ -1,3 +0,0 @@
import * as envs from '$env/static/private';
export const env = { ...envs, isProduction: process.env.NODE_ENV === 'production' };

View file

@ -1,22 +1,21 @@
import { Hono, type Schema } from 'hono';
import { setCookie } from 'hono/cookie'; import { setCookie } from 'hono/cookie';
import { inject, injectable } from 'tsyringe'; import { inject, injectable } from 'tsyringe';
import { zValidator } from '@hono/zod-validator'; import { zValidator } from '@hono/zod-validator';
import { IamService } from '../services/iam.service'; import { IamService } from '../services/iam.service';
import { LuciaProvider } from '../providers/lucia.provider';
import { signInEmailDto } from '../../../dtos/signin-email.dto';
import { updateEmailDto } from '../../../dtos/update-email.dto';
import { verifyEmailDto } from '../../../dtos/verify-email.dto';
import { registerEmailDto } from '../../../dtos/register-email.dto';
import { limiter } from '../middlewares/rate-limiter.middlware'; import { limiter } from '../middlewares/rate-limiter.middlware';
import { requireAuth } from '../middlewares/auth.middleware'; import { requireAuth } from '../middlewares/auth.middleware';
import { Controler } from '../common/classes/controller.class'; import { Controler } from '../common/types/controller';
import { registerEmailDto } from '$lib/server/api/dtos/register-email.dto';
import { signInEmailDto } from '$lib/server/api/dtos/signin-email.dto';
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
import { verifyEmailDto } from '$lib/server/api/dtos/verify-email.dto';
import { LuciaService } from '../services/lucia.service';
@injectable() @injectable()
export class IamController extends Controler { export class IamController extends Controler {
constructor( constructor(
@inject(IamService) private iamService: IamService, @inject(IamService) private iamService: IamService,
@inject(LuciaProvider) private lucia: LuciaProvider, @inject(LuciaService) private luciaService: LuciaService,
) { ) {
super(); super();
} }
@ -35,7 +34,7 @@ export class IamController extends Controler {
.post('/login/verify', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { .post('/login/verify', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const { email, token } = c.req.valid('json'); const { email, token } = c.req.valid('json');
const session = await this.iamService.verifyLoginRequest({ email, token }); const session = await this.iamService.verifyLoginRequest({ email, token });
const sessionCookie = this.lucia.createSessionCookie(session.id); const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id);
setCookie(c, sessionCookie.name, sessionCookie.value, { setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path, path: sessionCookie.attributes.path,
maxAge: sessionCookie.attributes.maxAge, maxAge: sessionCookie.attributes.maxAge,
@ -50,7 +49,7 @@ export class IamController extends Controler {
.post('/logout', requireAuth, async (c) => { .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.luciaService.lucia.createBlankSessionCookie();
setCookie(c, sessionCookie.name, sessionCookie.value, { setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path, path: sessionCookie.attributes.path,
maxAge: sessionCookie.attributes.maxAge, maxAge: sessionCookie.attributes.maxAge,

View file

@ -2,7 +2,7 @@ import { createId } from '@paralleldrive/cuid2';
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core'; import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm'; import { relations } from 'drizzle-orm';
import { usersTable } from './users.table'; import { usersTable } from './users.table';
import { timestamps } from '../../common/utils/table.utils'; import { timestamps } from '../../../common/utils/table';
export const emailVerificationsTable = pgTable('email_verifications', { export const emailVerificationsTable = pgTable('email_verifications', {
id: text('id') id: text('id')

View file

@ -1,5 +1,5 @@
import { bigint, pgTable, text } from "drizzle-orm/pg-core"; import { bigint, pgTable, text } from "drizzle-orm/pg-core";
import { cuid2, timestamps } from "../../common/utils/table.utils"; import { cuid2, timestamps } from "../../../common/utils/table";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
export const filesTable = pgTable('files', { export const filesTable = pgTable('files', {

View file

@ -1,7 +1,7 @@
import { relations } from 'drizzle-orm'; import { relations } from 'drizzle-orm';
import { createId } from '@paralleldrive/cuid2'; import { createId } from '@paralleldrive/cuid2';
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core'; import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
import { timestamps } from '../../common/utils/table.utils'; import { timestamps } from '../../../common/utils/table';
export const loginRequestsTable = pgTable('login_requests', { export const loginRequestsTable = pgTable('login_requests', {
id: text('id') id: text('id')

View file

@ -1,4 +1,4 @@
import { cuid2 } from '../../common/utils/table.utils'; import { cuid2 } from '../../../common/utils/table';
import { usersTable } from './users.table'; import { usersTable } from './users.table';
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core'; import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';

View file

@ -3,7 +3,7 @@ import { createId } from '@paralleldrive/cuid2';
import { sessionsTable } from './sessions.table'; import { sessionsTable } from './sessions.table';
import { boolean, pgTable, text } from 'drizzle-orm/pg-core'; import { boolean, pgTable, text } from 'drizzle-orm/pg-core';
import { emailVerificationsTable } from './email-verifications.table'; import { emailVerificationsTable } from './email-verifications.table';
import { citext, cuid2, timestamps } from '../../common/utils/table.utils'; import { citext, cuid2, timestamps } from '../../../common/utils/table';
import { filesTable } from './files.table'; import { filesTable } from './files.table';

View file

@ -0,0 +1,7 @@
import { Schema } from 'redis-om'
export const loginRequestSchema = new Schema('album', {
id: { type: 'string' },
hashedToken: { type: 'string' },
email: { type: 'string' },
})

View file

@ -3,4 +3,5 @@ import { z } from 'zod';
export const verifyEmailDto = z.object({ export const verifyEmailDto = z.object({
token: z.string() token: z.string()
}); });
export type VerifyEmailDto = z.infer<typeof verifyEmailDto>; export type VerifyEmailDto = z.infer<typeof verifyEmailDto>;

View file

@ -1,4 +1,4 @@
import type { Email } from "../common/inferfaces/email.interface" import type { Email } from "../common/types/email"
export class EmailChangeNoticeEmail implements Email { export class EmailChangeNoticeEmail implements Email {
constructor() { } constructor() { }

View file

@ -1,4 +1,4 @@
import type { Email } from "../common/inferfaces/email.interface" import type { Email } from "../common/types/email"
export class LoginVerificationEmail implements Email { export class LoginVerificationEmail implements Email {
constructor(private readonly token: string) { } constructor(private readonly token: string) { }

View file

@ -1,4 +1,4 @@
import type { Email } from "../common/inferfaces/email.interface"; import type { Email } from "../common/types/email";
export class WelcomeEmail implements Email { export class WelcomeEmail implements Email {
constructor() { } constructor() { }

View file

@ -3,7 +3,7 @@ import { Hono } from 'hono';
import { hc } from 'hono/client'; import { hc } from 'hono/client';
import { container } from 'tsyringe'; import { container } from 'tsyringe';
import { IamController } from './controllers/iam.controller'; import { IamController } from './controllers/iam.controller';
import { env } from './configs/envs.config'; import { config, env } from './common/config';
import { validateAuthSession, verifyOrigin } from './middlewares/auth.middleware'; import { validateAuthSession, verifyOrigin } from './middlewares/auth.middleware';
import { AuthCleanupJobs } from './jobs/auth-cleanup.job'; import { AuthCleanupJobs } from './jobs/auth-cleanup.job';
@ -32,7 +32,7 @@ container.resolve(AuthCleanupJobs).deleteStaleLoginRequests();
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Exports */ /* Exports */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
const rpc = hc<typeof routes>(env.ORIGIN); const rpc = hc<typeof routes>(config.api.origin);
export type ApiClient = typeof rpc; export type ApiClient = typeof rpc;
export type ApiRoutes = typeof routes; export type ApiRoutes = typeof routes;

View file

@ -1,17 +1,17 @@
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
import { JobsService } from "../services/jobs.service"; import { JobsService } from "../services/jobs.service";
// Example on how to create a job that runs once a week at midnight on Sunday
@injectable() @injectable()
export class AuthCleanupJobs { export class AuthCleanupJobs {
private queue; private queue;
constructor( constructor(@inject(JobsService) private jobsService: JobsService) {
@inject(JobsService) private jobsService: JobsService, // create queue
) { this.queue = this.jobsService.createQueue('auth_cleanup')
/* ------------------------------ Create Queue ------------------------------ */
this.queue = this.jobsService.createQueue('test')
/* ---------------------------- Register Workers ---------------------------- */ // register workers
this.worker(); this.worker();
} }

View file

@ -3,9 +3,14 @@ import { createMiddleware } from 'hono/factory';
import { verifyRequestOrigin } from 'lucia'; import { verifyRequestOrigin } from 'lucia';
import type { Session, User } from 'lucia'; import type { Session, User } from 'lucia';
import { Unauthorized } from '../common/exceptions'; import { Unauthorized } from '../common/exceptions';
import type { HonoTypes } from '../common/types/hono.type'; import type { HonoTypes } from '../common/types/hono';
import { lucia } from '../packages/lucia'; import { container } from 'tsyringe';
import { LuciaService } from '../services/lucia.service';
// resolve dependencies from the container
const { lucia } = container.resolve(LuciaService)
// Middleware to verify the origin of the request
export const verifyOrigin: MiddlewareHandler<HonoTypes> = createMiddleware(async (c, next) => { export const verifyOrigin: MiddlewareHandler<HonoTypes> = createMiddleware(async (c, next) => {
if (c.req.method === "GET") { if (c.req.method === "GET") {
return next(); return next();

View file

@ -1,11 +1,13 @@
import { rateLimiter } from "hono-rate-limiter"; import { rateLimiter } from "hono-rate-limiter";
import { RedisStore } from 'rate-limit-redis' import { RedisStore } from 'rate-limit-redis'
import RedisClient from 'ioredis' import type { HonoTypes } from "../common/types/hono";
import { env } from "../configs/envs.config"; import { container } from "tsyringe";
import type { HonoTypes } from "../common/types/hono.type"; import { RedisService } from '../services/redis.service';
const client = new RedisClient(env.REDIS_URL) // resolve dependencies from the container
const { client } = container.resolve(RedisService);
// Rate limiter middleware
export function limiter({ limit, minutes, key = "" }: { export function limiter({ limit, minutes, key = "" }: {
limit: number; limit: number;
minutes: number; minutes: number;
@ -23,8 +25,7 @@ export function limiter({ limit, minutes, key = "" }: {
}, // Method to generate custom identifiers for clients. }, // Method to generate custom identifiers for clients.
// Redis store configuration // Redis store configuration
store: new RedisStore({ store: new RedisStore({
// @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis sendCommand: (...args: string[]) => client.sendCommand(args),
sendCommand: (...args: string[]) => client.call(...args),
}) as any, }) as any,
}) })
} }

View file

@ -1,7 +0,0 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from '../databases/tables';
import { env } from '../configs/envs.config';
const client = postgres(env.DATABASE_URL!, { max: 1 });
export const db = drizzle(client, { schema });

View file

@ -1,22 +0,0 @@
import { Lucia } from 'lucia';
import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle';
import { sessionsTable, usersTable } from '../databases/tables';
import { env } from '../configs/envs.config';
import { db } from './drizzle';
const adapter = new DrizzlePostgreSQLAdapter(db, sessionsTable, usersTable);
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
// set to `true` when using HTTPS
secure: env.isProduction
}
},
getUserAttributes: (attributes) => {
return {
// attributes has the type of DatabaseUserAttributes
...attributes
};
}
});

View file

@ -1,12 +0,0 @@
import { S3Client } from '@aws-sdk/client-s3';
import { env } from '$env/dynamic/private';
export const s3Client = new S3Client({
region: 'auto',
endpoint: env.STORAGE_API_URL,
credentials: {
accessKeyId: env.STORAGE_API_ACCESS_KEY,
secretAccessKey: env.STORAGE_API_SECRET_KEY
},
forcePathStyle: true
})

View file

@ -1,6 +0,0 @@
import { container } from 'tsyringe';
import { db } from '../packages/drizzle';
export const DatabaseProvider = Symbol('DATABASE_TOKEN');
export type DatabaseProvider = typeof db;
container.register<DatabaseProvider>(DatabaseProvider, { useValue: db });

View file

@ -1,6 +0,0 @@
import { container } from 'tsyringe';
import { lucia } from '../packages/lucia';
export const LuciaProvider = Symbol('LUCIA_PROVIDER');
export type LuciaProvider = typeof lucia;
container.register<LuciaProvider>(LuciaProvider, { useValue: lucia });

View file

@ -1,11 +0,0 @@
import { container } from 'tsyringe';
import { env } from '../configs/envs.config';
import RedisClient from 'ioredis'
export const RedisProvider = Symbol('REDIS_TOKEN');
export type RedisProvider = RedisClient;
container.register<RedisProvider>(RedisProvider, {
useValue: new RedisClient(env.REDIS_URL, {
maxRetriesPerRequest: null
})
});

View file

@ -1,9 +0,0 @@
import { container } from 'tsyringe';
import { S3Client } from '@aws-sdk/client-s3';
import { s3Client } from '../packages/s3';
export const S3ClientProvider = Symbol('STORAGE_TOKEN');
export type S3ClientProvider = S3Client;
container.register<S3ClientProvider>(S3ClientProvider, {
useValue: s3Client
});

View file

@ -1,39 +1,35 @@
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
import { and, eq, gte, lte, type InferInsertModel } from "drizzle-orm"; import { and, eq, gte, type InferInsertModel } from "drizzle-orm";
import { emailVerificationsTable } from "../databases/tables"; import { emailVerificationsTable } from "../databases/postgres/tables";
import type { Repository } from "../common/inferfaces/repository.interface"; import { takeFirst, takeFirstOrThrow } from "../common/utils/repository";
import { DatabaseProvider } from "../providers/database.provider"; import { DrizzleService } from "../services/drizzle.service";
import { takeFirst, takeFirstOrThrow } from "../common/utils/repository.utils";
export type CreateEmailVerification = Pick<InferInsertModel<typeof emailVerificationsTable>, 'requestedEmail' | 'hashedToken' | 'userId' | 'expiresAt'>; export type CreateEmailVerification = Pick<InferInsertModel<typeof emailVerificationsTable>, 'requestedEmail' | 'hashedToken' | 'userId' | 'expiresAt'>;
@injectable() @injectable()
export class EmailVerificationsRepository implements Repository { export class EmailVerificationsRepository {
constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) { } constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) { }
// creates a new email verification record or updates an existing one // creates a new email verification record or updates an existing one
async create(data: CreateEmailVerification) { async create(data: CreateEmailVerification) {
return this.db.insert(emailVerificationsTable).values(data).onConflictDoUpdate({ return this.drizzle.db.insert(emailVerificationsTable).values(data).onConflictDoUpdate({
target: emailVerificationsTable.userId, target: emailVerificationsTable.userId,
set: data set: data
}).returning().then(takeFirstOrThrow) }).returning().then(takeFirstOrThrow)
} }
// finds a valid record by token and userId // finds a valid record by token and userId
async findValidRecord(userId: string) { async findValidRecord(userId: string, db = this.drizzle.db) {
return this.db.select().from(emailVerificationsTable).where( return db.select().from(emailVerificationsTable).where(
and( and(
eq(emailVerificationsTable.userId, userId), eq(emailVerificationsTable.userId, userId),
gte(emailVerificationsTable.expiresAt, new Date()) gte(emailVerificationsTable.expiresAt, new Date())
)).then(takeFirst) )).then(takeFirst)
} }
async deleteById(id: string) { async deleteById(id: string, db = this.drizzle.db) {
return this.db.delete(emailVerificationsTable).where(eq(emailVerificationsTable.id, id)) return db.delete(emailVerificationsTable).where(eq(emailVerificationsTable.id, id))
} }
trxHost(trx: DatabaseProvider) {
return new EmailVerificationsRepository(trx)
}
} }

View file

@ -1,29 +1,29 @@
import { inject } from "tsyringe"; import { inject } from "tsyringe";
import { StorageService } from "../services/storage.service"; import { StorageService } from "../services/storage.service";
import { DatabaseProvider } from "../providers/database.provider";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { filesTable } from "../databases/tables/files.table"; import { filesTable } from "../databases/postgres/tables/files.table";
import { takeFirst, takeFirstOrThrow } from "../common/utils/repository.utils"; import { takeFirst, takeFirstOrThrow } from "../common/utils/repository";
import { DrizzleService } from "../services/drizzle.service";
export class FilesRepository { export class FilesRepository {
constructor( constructor(
@inject(StorageService) private readonly storageService: StorageService, @inject(StorageService) private readonly storageService: StorageService,
@inject(DatabaseProvider) private readonly db: DatabaseProvider) { } @inject(DrizzleService) private readonly drizzle: DrizzleService) { }
async create(file: File, db = this.db) { async create(file: File, db = this.drizzle.db) {
const asset = await this.storageService.upload(file); const asset = await this.storageService.upload(file);
return db.insert(filesTable).values({ key: asset.key, contentType: asset.type, size: BigInt(asset.size) }).returning().then(takeFirst) return db.insert(filesTable).values({ key: asset.key, contentType: asset.type, size: BigInt(asset.size) }).returning().then(takeFirst)
} }
async findOneById(id: string, db = this.db) { async findOneById(id: string, db = this.drizzle.db) {
return db.select().from(filesTable).where(eq(filesTable.id, id)).then(takeFirst) return db.select().from(filesTable).where(eq(filesTable.id, id)).then(takeFirst)
} }
async findOneByIdOrThrow(id: string, db = this.db) { async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
return db.select().from(filesTable).where(eq(filesTable.id, id)).then(takeFirstOrThrow) return db.select().from(filesTable).where(eq(filesTable.id, id)).then(takeFirstOrThrow)
} }
async update(id: string, file: File, db = this.db) { async update(id: string, file: File, db = this.drizzle.db) {
// upload new file // upload new file
const newAsset = await this.storageService.upload(file); const newAsset = await this.storageService.upload(file);
await db.update(filesTable).set({ key: newAsset.key, contentType: newAsset.type, size: BigInt(newAsset.size) }).where(eq(filesTable.id, id)) await db.update(filesTable).set({ key: newAsset.key, contentType: newAsset.type, size: BigInt(newAsset.size) }).where(eq(filesTable.id, id))
@ -33,7 +33,7 @@ export class FilesRepository {
await this.storageService.delete(oldAsset.key) await this.storageService.delete(oldAsset.key)
} }
async delete(id: string, db = this.db) { async delete(id: string, db = this.drizzle.db) {
const asset = await this.findOneByIdOrThrow(id) const asset = await this.findOneByIdOrThrow(id)
await this.storageService.delete(asset.key) await this.storageService.delete(asset.key)
await db.delete(filesTable).where(eq(filesTable.id, id)) await db.delete(filesTable).where(eq(filesTable.id, id))

View file

@ -1,26 +1,27 @@
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
import { and, eq, gte, type InferInsertModel } from "drizzle-orm"; import { and, eq, gte, type InferInsertModel } from "drizzle-orm";
import { loginRequestsTable } from "../databases/tables"; import { loginRequestsTable } from "../databases/postgres/tables";
import type { Repository } from "../common/inferfaces/repository.interface"; import { takeFirst, takeFirstOrThrow } from "../common/utils/repository";
import { DatabaseProvider } from "../providers/database.provider"; import { DrizzleService } from "../services/drizzle.service";
import { takeFirst, takeFirstOrThrow } from "../common/utils/repository.utils";
export type CreateLoginRequest = Pick<InferInsertModel<typeof loginRequestsTable>, 'email' | 'expiresAt' | 'hashedToken'>; export type CreateLoginRequest = Pick<InferInsertModel<typeof loginRequestsTable>, 'email' | 'expiresAt' | 'hashedToken'>;
@injectable() @injectable()
export class LoginRequestsRepository implements Repository { export class LoginRequestsRepository {
constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) { } constructor(
@inject(DrizzleService) private readonly drizzle: DrizzleService,
) { }
async create(data: CreateLoginRequest) { async create(data: CreateLoginRequest, db = this.drizzle.db) {
return this.db.insert(loginRequestsTable).values(data).onConflictDoUpdate({ return db.insert(loginRequestsTable).values(data).onConflictDoUpdate({
target: loginRequestsTable.email, target: loginRequestsTable.email,
set: data set: data
}).returning().then(takeFirstOrThrow) }).returning().then(takeFirstOrThrow)
} }
async findOneByEmail(email: string) { async findOneByEmail(email: string, db = this.drizzle.db) {
return this.db.select().from(loginRequestsTable).where( return db.select().from(loginRequestsTable).where(
and( and(
eq(loginRequestsTable.email, email), eq(loginRequestsTable.email, email),
gte(loginRequestsTable.expiresAt, new Date()) gte(loginRequestsTable.expiresAt, new Date())
@ -28,11 +29,7 @@ export class LoginRequestsRepository implements Repository {
).then(takeFirst) ).then(takeFirst)
} }
async deleteById(id: string) { async deleteById(id: string, db = this.drizzle.db) {
return this.db.delete(loginRequestsTable).where(eq(loginRequestsTable.id, id)); return db.delete(loginRequestsTable).where(eq(loginRequestsTable.id, id));
}
trxHost(trx: DatabaseProvider) {
return new LoginRequestsRepository(trx);
} }
} }

View file

@ -1,49 +1,45 @@
import { inject, injectable } from 'tsyringe'; import { inject, injectable } from 'tsyringe';
import { usersTable } from '../databases/tables'; import { usersTable } from '../databases/postgres/tables';
import { eq, type InferInsertModel } from 'drizzle-orm'; import { eq, type InferInsertModel } from 'drizzle-orm';
import { DatabaseProvider } from '../providers/database.provider'; import { takeFirstOrThrow } from '../common/utils/repository';
import { takeFirstOrThrow } from '../common/utils/repository.utils';
import type { Repository } from '../common/inferfaces/repository.interface'; import type { Repository } from '../common/inferfaces/repository.interface';
import { DrizzleService } from '../services/drizzle.service';
export type CreateUser = InferInsertModel<typeof usersTable>; export type CreateUser = InferInsertModel<typeof usersTable>;
export type UpdateUser = Partial<CreateUser>; export type UpdateUser = Partial<CreateUser>;
@injectable() @injectable()
export class UsersRepository implements Repository { export class UsersRepository {
constructor(@inject(DatabaseProvider) private db: DatabaseProvider) { } constructor(@inject(DrizzleService) private drizzle: DrizzleService) { }
async findOneById(id: string) { async findOneById(id: string, db = this.drizzle.db) {
return this.db.query.usersTable.findFirst({ return db.query.usersTable.findFirst({
where: eq(usersTable.id, id) where: eq(usersTable.id, id)
}); });
} }
async findOneByIdOrThrow(id: string) { async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
const user = await this.findOneById(id); const user = await this.findOneById(id, db);
if (!user) throw Error('User not found'); if (!user) throw Error('User not found');
return user; return user;
} }
async findOneByEmail(email: string) { async findOneByEmail(email: string, db = this.drizzle.db) {
return this.db.query.usersTable.findFirst({ return db.query.usersTable.findFirst({
where: eq(usersTable.email, email) where: eq(usersTable.email, email)
}); });
} }
async create(data: CreateUser) { async create(data: CreateUser, db = this.drizzle.db) {
return this.db.insert(usersTable).values(data).returning().then(takeFirstOrThrow); return db.insert(usersTable).values(data).returning().then(takeFirstOrThrow);
} }
async update(id: string, data: UpdateUser) { async update(id: string, data: UpdateUser, db = this.drizzle.db) {
return this.db return db
.update(usersTable) .update(usersTable)
.set(data) .set(data)
.where(eq(usersTable.id, id)) .where(eq(usersTable.id, id))
.returning() .returning()
.then(takeFirstOrThrow); .then(takeFirstOrThrow);
} }
trxHost(trx: DatabaseProvider) {
return new UsersRepository(trx);
}
} }

View file

@ -0,0 +1,22 @@
import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { injectable, type Disposable } from "tsyringe";
import { config } from "../common/config";
import * as schema from '../databases/postgres/tables';
@injectable()
export class DrizzleService implements Disposable {
protected readonly client: postgres.Sql<{}>
readonly db: PostgresJsDatabase<typeof schema>
readonly schema: typeof schema = schema;
constructor() {
const client = postgres(config.postgres.url, { max: 1 })
this.client = client;
this.db = drizzle(client, { schema })
}
dispose(): Promise<void> | void {
this.client.end();
}
}

View file

@ -1,23 +1,23 @@
import { inject, injectable } from 'tsyringe'; import { inject, injectable } from 'tsyringe';
import { MailerService } from './mailer.service'; import { MailerService } from './mailer.service';
import { TokensService } from './tokens.service'; import { TokensService } from './tokens.service';
import { LuciaProvider } from '../providers/lucia.provider';
import { UsersRepository } from '../repositories/users.repository'; import { UsersRepository } from '../repositories/users.repository';
import type { SignInEmailDto } from '../dtos/signin-email.dto'; import type { SignInEmailDto } from '../dtos/signin-email.dto';
import type { RegisterEmailDto } from '../dtos/register-email.dto'; import type { RegisterEmailDto } from '../dtos/register-email.dto';
import { LoginRequestsRepository } from '../repositories/login-requests.repository'; import { LoginRequestsRepository } from '../repositories/login-requests.repository';
import { LoginVerificationEmail } from '../emails/login-verification.email'; import { LoginVerificationEmail } from '../emails/login-verification.email';
import { DatabaseProvider } from '../providers/database.provider';
import { BadRequest } from '../common/exceptions'; import { BadRequest } from '../common/exceptions';
import { WelcomeEmail } from '../emails/welcome.email'; import { WelcomeEmail } from '../emails/welcome.email';
import { EmailVerificationsRepository } from '../repositories/email-verifications.repository'; import { EmailVerificationsRepository } from '../repositories/email-verifications.repository';
import { EmailChangeNoticeEmail } from '../emails/email-change-notice.email'; import { EmailChangeNoticeEmail } from '../emails/email-change-notice.email';
import { DrizzleService } from './drizzle.service';
import { LuciaService } from './lucia.service';
@injectable() @injectable()
export class IamService { export class IamService {
constructor( constructor(
@inject(LuciaProvider) private readonly lucia: LuciaProvider, @inject(LuciaService) private readonly luciaService: LuciaService,
@inject(DatabaseProvider) private readonly db: DatabaseProvider, @inject(DrizzleService) private readonly drizzleService: DrizzleService,
@inject(TokensService) private readonly tokensService: TokensService, @inject(TokensService) private readonly tokensService: TokensService,
@inject(MailerService) private readonly mailerService: MailerService, @inject(MailerService) private readonly mailerService: MailerService,
@inject(UsersRepository) private readonly usersRepository: UsersRepository, @inject(UsersRepository) private readonly usersRepository: UsersRepository,
@ -42,10 +42,10 @@ export class IamService {
if (!existingUser) { if (!existingUser) {
const newUser = await this.handleNewUserRegistration(data.email); const newUser = await this.handleNewUserRegistration(data.email);
return this.lucia.createSession(newUser.id, {}); return this.luciaService.lucia.createSession(newUser.id, {});
} }
return this.lucia.createSession(existingUser.id, {}); return this.luciaService.lucia.createSession(existingUser.id, {});
} }
// These steps follow the process outlined in OWASP's "Changing A User's Email Address" guide. // These steps follow the process outlined in OWASP's "Changing A User's Email Address" guide.
@ -80,7 +80,7 @@ export class IamService {
} }
async logout(sessionId: string) { async logout(sessionId: string) {
return this.lucia.invalidateSession(sessionId); return this.luciaService.lucia.invalidateSession(sessionId);
} }
// Create a new user and send a welcome email - or other onboarding process // Create a new user and send a welcome email - or other onboarding process
@ -93,9 +93,9 @@ export class IamService {
// Fetch a valid request from the database, verify the token and burn the request if it is valid // Fetch a valid request from the database, verify the token and burn the request if it is valid
private async getValidLoginRequest(email: string, token: string) { private async getValidLoginRequest(email: string, token: string) {
return await this.db.transaction(async (trx) => { return await this.drizzleService.db.transaction(async (trx) => {
// fetch the login request // fetch the login request
const loginRequest = await this.loginRequestsRepository.trxHost(trx).findOneByEmail(email) const loginRequest = await this.loginRequestsRepository.findOneByEmail(email, trx)
if (!loginRequest) return null; if (!loginRequest) return null;
// check if the token is valid // check if the token is valid
@ -103,15 +103,15 @@ export class IamService {
if (!isValidRequest) return null if (!isValidRequest) return null
// if the token is valid, burn the request // if the token is valid, burn the request
await this.loginRequestsRepository.trxHost(trx).deleteById(loginRequest.id); await this.loginRequestsRepository.deleteById(loginRequest.id, trx);
return loginRequest return loginRequest
}) })
} }
private async findAndBurnEmailVerificationToken(userId: string, token: string) { private async findAndBurnEmailVerificationToken(userId: string, token: string) {
return this.db.transaction(async (trx) => { return this.drizzleService.db.transaction(async (trx) => {
// find a valid record // find a valid record
const emailVerificationRecord = await this.emailVerificationsRepository.trxHost(trx).findValidRecord(userId); const emailVerificationRecord = await this.emailVerificationsRepository.findValidRecord(userId, trx);
if (!emailVerificationRecord) return null; if (!emailVerificationRecord) return null;
// check if the token is valid // check if the token is valid
@ -119,7 +119,7 @@ export class IamService {
if (!isValidRecord) return null if (!isValidRecord) return null
// burn the token if it is valid // burn the token if it is valid
await this.emailVerificationsRepository.trxHost(trx).deleteById(emailVerificationRecord.id) await this.emailVerificationsRepository.deleteById(emailVerificationRecord.id, trx)
return emailVerificationRecord return emailVerificationRecord
}) })
} }

View file

@ -1,17 +1,28 @@
import { inject, injectable } from "tsyringe"; import { injectable } from "tsyringe";
import { Queue, Worker, type Processor } from 'bullmq'; import { Queue, Worker, type Processor } from 'bullmq';
import { RedisProvider } from "../providers/redis.provider"; import RedisClient from "ioredis";
import { config } from "../common/config";
// BullMQ utilizes ioredis, which is no longer maintained but still works fine.
// I recommend using BullMQ with ioredis for now, but keep an eye out for future updates.
@injectable() @injectable()
export class JobsService { export class JobsService {
constructor(@inject(RedisProvider) private readonly redis: RedisProvider) { constructor() { }
}
createQueue(name: string) { createQueue(name: string) {
return new Queue(name, { connection: this.redis }) return new Queue(name, {
connection: new RedisClient(config.redis.url, {
maxRetriesPerRequest: null
})
})
} }
createWorker(name: string, prcoessor: Processor) { createWorker(name: string, prcoessor: Processor) {
return new Worker(name, prcoessor, { connection: this.redis }) return new Worker(name, prcoessor, {
connection: new RedisClient(config.redis.url, {
maxRetriesPerRequest: null
})
})
} }
} }

View file

@ -0,0 +1,25 @@
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { Lucia } from "lucia";
import { inject, injectable } from "tsyringe";
import { DrizzleService } from "./drizzle.service";
import { config } from "../common/config";
@injectable()
export class LuciaService {
readonly lucia: Lucia;
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {
const adapter = new DrizzlePostgreSQLAdapter(this.drizzle.db, this.drizzle.schema.sessionsTable, this.drizzle.schema.usersTable);
this.lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: config.isProduction
}
},
getUserAttributes: (attributes) => {
return {
...attributes
};
}
});
}
}

View file

@ -1,6 +1,6 @@
import { injectable } from 'tsyringe'; import { injectable } from 'tsyringe';
import { env } from '../configs/envs.config';
import type { Email } from '../common/inferfaces/email.interface'; import type { Email } from '../common/inferfaces/email.interface';
import { config } from '../common/config';
type SendProps = { type SendProps = {
to: string | string[]; to: string | string[];
@ -11,7 +11,7 @@ type SendProps = {
export class MailerService { export class MailerService {
async send(data: SendProps) { async send(data: SendProps) {
const mailer = env.isProduction ? this.sendProd : this.sendDev; const mailer = config.isProduction ? this.sendProd : this.sendDev;
await mailer(data); await mailer(data);
} }

View file

@ -0,0 +1,42 @@
import { createClient, type RedisClientType } from "redis";
import { injectable, type Disposable } from "tsyringe";
import { config } from "../common/config";
import type { AsyncService } from "../common/inferfaces/async-service.interface";
@injectable()
export class RedisService implements Disposable, AsyncService {
readonly client: RedisClientType;
private isConnected: boolean = false;
constructor() {
this.client = createClient({
url: config.redis.url,
});
this.init();
}
async ensureConnected(): Promise<void> {
if (!this.isConnected) {
await this.init();
}
}
async init(): Promise<void> {
try {
await this.client.connect();
this.isConnected = this.client.isReady;
console.log('Redis connected');
} catch (error) {
console.error('Failed to connect to Redis:', error);
throw error;
}
}
async dispose(): Promise<void> {
if (this.isConnected) {
await this.client.disconnect();
this.isConnected = false;
console.log('Redis disconnected');
}
}
}

View file

@ -1,17 +1,28 @@
import { PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; import { PutObjectCommand, DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { createId } from '@paralleldrive/cuid2'; import { createId } from '@paralleldrive/cuid2';
import { inject, injectable } from 'tsyringe'; import { injectable } from 'tsyringe';
import { env } from '../configs/envs.config'; import { config } from '../common/config';
import { S3ClientProvider } from '../providers/s3.provider';
@injectable() @injectable()
export class StorageService { export class StorageService {
constructor(@inject(S3ClientProvider) private readonly s3Client: S3ClientProvider) { } protected readonly s3Client: S3Client
constructor() {
this.s3Client = new S3Client({
region: 'auto',
endpoint: config.storage.url,
credentials: {
accessKeyId: config.storage.accessKey,
secretAccessKey: config.storage.secretKey
},
forcePathStyle: true
})
}
async upload(file: File) { async upload(file: File) {
const key = createId(); const key = createId();
const uploadCommand = new PutObjectCommand({ const uploadCommand = new PutObjectCommand({
Bucket: env.STORAGE_BUCKET_NAME, Bucket: config.storage.bucket,
ACL: 'public-read', ACL: 'public-read',
Key: key, Key: key,
ContentType: file.type, ContentType: file.type,
@ -24,7 +35,7 @@ export class StorageService {
delete(key: string) { delete(key: string) {
const deleteCommand = new DeleteObjectCommand({ const deleteCommand = new DeleteObjectCommand({
Bucket: env.STORAGE_BUCKET_NAME, Bucket: config.storage.bucket,
Key: key Key: key
}); });

View file

@ -1,23 +1,22 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { LoginRequestsService } from '../services/login-requests.service';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { TokensService } from '../services/tokens.service'; import { TokensService } from '../services/tokens.service';
import { MailerService } from '../services/mailer.service'; import { MailerService } from '../services/mailer.service';
import { UsersRepository } from '../repositories/users.repository'; import { UsersRepository } from '../repositories/users.repository';
import { LoginRequestsRepository } from '../repositories/login-requests.repository'; import { LoginRequestsRepository } from '../repositories/login-requests.repository';
import { PgDatabase } from 'drizzle-orm/pg-core';
import { container } from 'tsyringe'; import { container } from 'tsyringe';
import { LuciaProvider } from '../providers/lucia.provider'; import { LuciaService } from '../services/lucia.service';
import { DatabaseProvider } from '../providers/database.provider'; import { DrizzleService } from '../services/drizzle.service';
import { IamService } from '../services/iam.service';
describe('LoginRequestService', () => { describe('LoginRequestService', () => {
let service: LoginRequestsService; let service: IamService;
let tokensService = vi.mocked(TokensService.prototype) let tokensService = vi.mocked(TokensService.prototype)
let mailerService = vi.mocked(MailerService.prototype); let mailerService = vi.mocked(MailerService.prototype);
let usersRepository = vi.mocked(UsersRepository.prototype); let usersRepository = vi.mocked(UsersRepository.prototype);
let loginRequestsRepository = vi.mocked(LoginRequestsRepository.prototype); let loginRequestsRepository = vi.mocked(LoginRequestsRepository.prototype);
let luciaProvider = vi.mocked(LuciaProvider); let luciaService = vi.mocked(LuciaService.prototype);
let databaseProvider = vi.mocked(PgDatabase); let drizzleService = vi.mocked(DrizzleService.prototype);
beforeAll(() => { beforeAll(() => {
service = container service = container
@ -25,9 +24,9 @@ describe('LoginRequestService', () => {
.register<MailerService>(MailerService, { useValue: mailerService }) .register<MailerService>(MailerService, { useValue: mailerService })
.register<UsersRepository>(UsersRepository, { useValue: usersRepository }) .register<UsersRepository>(UsersRepository, { useValue: usersRepository })
.register(LoginRequestsRepository, { useValue: loginRequestsRepository }) .register(LoginRequestsRepository, { useValue: loginRequestsRepository })
.register(LuciaProvider, { useValue: luciaProvider }) .register(LuciaService, { useValue: luciaService })
.register(DatabaseProvider, { useValue: databaseProvider }) .register(DrizzleService, { useValue: drizzleService })
.resolve(LoginRequestsService); .resolve(IamService);
}); });
afterAll(() => { afterAll(() => {
@ -57,7 +56,7 @@ describe('LoginRequestService', () => {
const spy_loginRequestsRepository_create = vi.spyOn(loginRequestsRepository, 'create') const spy_loginRequestsRepository_create = vi.spyOn(loginRequestsRepository, 'create')
it('should resolve', async () => { it('should resolve', async () => {
await expect(service.create({ email: "test" })).resolves.toBeUndefined() await expect(service.createLoginRequest({ email: "test" })).resolves.toBeUndefined()
}) })
it('should generate a token with expiry and hash', async () => { it('should generate a token with expiry and hash', async () => {
expect(spy_tokensService_generateTokenWithExpiryAndHash).toBeCalledTimes(1) expect(spy_tokensService_generateTokenWithExpiryAndHash).toBeCalledTimes(1)

View file

@ -1,29 +1,28 @@
import { zod } from "sveltekit-superforms/adapters"; import { zod } from "sveltekit-superforms/adapters";
import { updateEmailDto } from "$lib/dtos/update-email.dto.js";
import { verifyEmailDto } from "$lib/dtos/verify-email.dto.js";
import { fail, setError, superValidate } from "sveltekit-superforms"; import { fail, setError, superValidate } from "sveltekit-superforms";
import { StatusCodes } from "$lib/constants/status-codes.js"; import { StatusCodes } from "$lib/constants/status-codes.js";
import { updateEmailFormSchema, verifyEmailFormSchema } from "./schemas.js";
export let load = async (event) => { export let load = async (event) => {
const authedUser = await event.locals.getAuthedUserOrThrow() const authedUser = await event.locals.getAuthedUserOrThrow()
return { return {
authedUser, authedUser,
updateEmailForm: await superValidate(authedUser, zod(updateEmailDto)), updateEmailForm: await superValidate(authedUser, zod(updateEmailFormSchema)),
verifyEmailForm: await superValidate(zod(verifyEmailDto)) verifyEmailForm: await superValidate(zod(verifyEmailFormSchema))
}; };
}; };
export const actions = { export const actions = {
updateEmail: async ({ request, locals }) => { updateEmail: async ({ request, locals }) => {
const updateEmailForm = await superValidate(request, zod(updateEmailDto)); const updateEmailForm = await superValidate(request, zod(updateEmailFormSchema));
if (!updateEmailForm.valid) return fail(StatusCodes.BAD_REQUEST, { updateEmailForm }) if (!updateEmailForm.valid) return fail(StatusCodes.BAD_REQUEST, { updateEmailForm })
const { error } = await locals.api.iam.email.$patch({ json: updateEmailForm.data }).then(locals.parseApiResponse); const { error } = await locals.api.iam.email.$patch({ json: updateEmailForm.data }).then(locals.parseApiResponse);
if (error) return setError(updateEmailForm, 'email', error); if (error) return setError(updateEmailForm, 'email', error);
return { updateEmailForm } return { updateEmailForm }
}, },
verifyEmail: async ({ request, locals }) => { verifyEmail: async ({ request, locals }) => {
const verifyEmailForm = await superValidate(request, zod(verifyEmailDto)); const verifyEmailForm = await superValidate(request, zod(verifyEmailFormSchema));
console.log(verifyEmailForm) console.log(verifyEmailForm)
if (!verifyEmailForm.valid) return fail(StatusCodes.BAD_REQUEST, { verifyEmailForm }) if (!verifyEmailForm.valid) return fail(StatusCodes.BAD_REQUEST, { verifyEmailForm })
const { error } = await locals.api.iam.email.verification.$post({ json: verifyEmailForm.data }).then(locals.parseApiResponse); const { error } = await locals.api.iam.email.verification.$post({ json: verifyEmailForm.data }).then(locals.parseApiResponse);

View file

@ -0,0 +1,9 @@
import { z } from "zod";
export const verifyEmailFormSchema = z.object({
token: z.string()
});
export const updateEmailFormSchema = z.object({
email: z.string().email()
});

View file

@ -1,7 +1,5 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import type { SuperValidated, Infer } from 'sveltekit-superforms'; import type { SuperValidated, Infer } from 'sveltekit-superforms';
import type { updateEmailDto } from '$lib/dtos/update-email.dto';
import type { verifyEmailDto } from '$lib/dtos/verify-email.dto';
interface UpdateEmailCardProps { interface UpdateEmailCardProps {
updateEmailForm: SuperValidated<Infer<typeof updateEmailDto>>; updateEmailForm: SuperValidated<Infer<typeof updateEmailDto>>;
@ -16,6 +14,8 @@
import { superForm } from 'sveltekit-superforms'; import { superForm } from 'sveltekit-superforms';
import * as Dialog from '$lib/components/ui/dialog'; import * as Dialog from '$lib/components/ui/dialog';
import PinInput from '$lib/components/pin-input.svelte'; import PinInput from '$lib/components/pin-input.svelte';
import type { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
import type { verifyEmailDto } from '$lib/server/api/dtos/verify-email.dto';
/* ---------------------------------- props --------------------------------- */ /* ---------------------------------- props --------------------------------- */
let { updateEmailForm, verifyEmailForm }: UpdateEmailCardProps = $props(); let { updateEmailForm, verifyEmailForm }: UpdateEmailCardProps = $props();

View file

@ -1,27 +1,26 @@
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import { zod } from 'sveltekit-superforms/adapters'; import { zod } from 'sveltekit-superforms/adapters';
import { signInEmailDto } from '$lib/dtos/signin-email.dto.js';
import { setError, superValidate } from 'sveltekit-superforms';
import { registerEmailDto } from '$lib/dtos/register-email.dto.js';
import { StatusCodes } from '$lib/constants/status-codes'; import { StatusCodes } from '$lib/constants/status-codes';
import { setError, superValidate } from 'sveltekit-superforms';
import { registerFormSchema, signInFormSchema } from './schemas';
export const load = async () => { export const load = async () => {
return { return {
emailRegisterForm: await superValidate(zod(registerEmailDto)), emailRegisterForm: await superValidate(zod(registerFormSchema)),
emailSigninForm: await superValidate(zod(signInEmailDto)) emailSigninForm: await superValidate({email: 'test'}, zod(signInFormSchema))
}; };
}; };
export const actions = { export const actions = {
register: async ({ locals, request }) => { register: async ({ locals, request }) => {
const emailRegisterForm = await superValidate(request, zod(registerEmailDto)); const emailRegisterForm = await superValidate(request, zod(registerFormSchema));
if (!emailRegisterForm.valid) return fail(StatusCodes.BAD_REQUEST, { emailRegisterForm }); if (!emailRegisterForm.valid) return fail(StatusCodes.BAD_REQUEST, { emailRegisterForm });
const { error } = await locals.api.iam.login.request.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse); const { error } = await locals.api.iam.login.request.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse);
if (error) return setError(emailRegisterForm, 'email', error); if (error) return setError(emailRegisterForm, 'email', error);
return { emailRegisterForm }; return { emailRegisterForm };
}, },
signin: async ({ locals, request }) => { signin: async ({ locals, request }) => {
const emailSignInForm = await superValidate(request, zod(signInEmailDto)); const emailSignInForm = await superValidate(request, zod(signInFormSchema));
if (!emailSignInForm.valid) return fail(StatusCodes.BAD_REQUEST, { emailSignInForm }); if (!emailSignInForm.valid) return fail(StatusCodes.BAD_REQUEST, { emailSignInForm });
const { error } = await locals.api.iam.login.verify.$post({ json: emailSignInForm.data }).then(locals.parseApiResponse) const { error } = await locals.api.iam.login.verify.$post({ json: emailSignInForm.data }).then(locals.parseApiResponse)
if (error) return setError(emailSignInForm, 'token', error); if (error) return setError(emailSignInForm, 'token', error);

View file

@ -1,41 +1,19 @@
<!-- <script>
import { enhance } from '$app/forms';
</script>
<h3>Register</h3>
<form action="?/register" method="POST" use:enhance>
<label for="email">Email</label>
<input name="email" type="email" />
<button type="submit">Register</button>
</form>
<h3>Verify</h3>
<form action="?/signin" method="POST" use:enhance>
<label for="email">Email</label>
<input name="email" type="email" />
<label for="token">Token</label>
<input name="token" type="text" />
<button type="submit">Login</button>
</form> -->
<script lang="ts"> <script lang="ts">
import { Button } from "$lib/components/ui/button/index.js"; import { Button } from '$lib/components/ui/button/index.js';
import * as Card from "$lib/components/ui/card/index.js"; import * as Card from '$lib/components/ui/card/index.js';
import { Input } from "$lib/components/ui/input/index.js"; import { Input } from '$lib/components/ui/input/index.js';
import { Label } from "$lib/components/ui/label/index.js";
import { superForm } from 'sveltekit-superforms'; import { superForm } from 'sveltekit-superforms';
import * as Form from '$lib/components/ui/form'; import * as Form from '$lib/components/ui/form';
import { zodClient } from 'sveltekit-superforms/adapters'; import { zodClient } from 'sveltekit-superforms/adapters';
import { registerEmailDto } from "$lib/dtos/register-email.dto.js"; import PinInput from '$lib/components/pin-input.svelte';
import { signInEmailDto } from "$lib/dtos/signin-email.dto.js"; import { registerFormSchema, signInFormSchema } from './schemas.js';
import PinInput from "$lib/components/pin-input.svelte";
const {data} = $props(); const { data } = $props();
let showTokenVerification = $state(false); let showTokenVerification = $state(false);
const emailRegisterForm = superForm(data.emailRegisterForm, { const emailRegisterForm = superForm(data.emailRegisterForm, {
validators: zodClient(registerEmailDto), validators: zodClient(registerFormSchema),
resetForm: false, resetForm: false,
onUpdated: ({ form }) => { onUpdated: ({ form }) => {
if (form.valid) { if (form.valid) {
@ -46,13 +24,12 @@
}); });
const emailSigninForm = superForm(data.emailSigninForm, { const emailSigninForm = superForm(data.emailSigninForm, {
validators: zodClient(signInEmailDto), validators: zodClient(signInFormSchema),
resetForm: false resetForm: false
}); });
const { form: emailRegisterFormData, enhance: emailRegisterEnhance } = emailRegisterForm; const { form: emailRegisterFormData, enhance: emailRegisterEnhance } = emailRegisterForm;
const { form: emailSigninFormData, enhance: emailSigninEnhance } = emailSigninForm; const { form: emailSigninFormData, enhance: emailSigninEnhance } = emailSigninForm;
</script> </script>
<Card.Root class="mx-auto mt-24 max-w-sm"> <Card.Root class="mx-auto mt-24 max-w-sm">

View file

@ -0,0 +1,12 @@
import { z } from "zod";
export const registerFormSchema = z.object({
email: z.string().email()
});
export const signInFormSchema = z.object({
email: z.string().email(),
token: z.string()
});