mirror of
https://github.com/BradNut/TofuStack
synced 2025-09-08 17:40:26 +00:00
Merge branch 'feature/project-restructure'
This commit is contained in:
commit
53ebe9a157
68 changed files with 575 additions and 375 deletions
12
.env.example
12
.env.example
|
|
@ -1,5 +1,15 @@
|
|||
# API
|
||||
ORIGIN=http://localhost:5173
|
||||
|
||||
# Database
|
||||
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
3
Dockerfile.minio
Normal 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
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { Config } from 'drizzle-kit';
|
||||
|
||||
export default {
|
||||
out: './src/lib/server/api/databases/migrations',
|
||||
schema: './src/lib/server/api/databases/tables/*.table.ts',
|
||||
out: './src/lib/server/api/databases/postgres/migrations',
|
||||
schema: './src/lib/server/api/databases/postgres/tables/*.table.ts',
|
||||
breakpoints: false,
|
||||
strict: true,
|
||||
dialect: 'postgresql',
|
||||
|
|
|
|||
41
minio-console.conf.template
Normal file
41
minio-console.conf.template
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +87,8 @@
|
|||
"mode-watcher": "^0.4.1",
|
||||
"paneforge": "^0.0.5",
|
||||
"rate-limit-redis": "^4.2.0",
|
||||
"redis": "^4.7.0",
|
||||
"redis-om": "^0.4.6",
|
||||
"resend": "^3.5.0",
|
||||
"svelte-sonner": "^0.3.27",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
|
|
|
|||
107
pnpm-lock.yaml
107
pnpm-lock.yaml
|
|
@ -38,6 +38,12 @@ importers:
|
|||
rate-limit-redis:
|
||||
specifier: ^4.2.0
|
||||
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:
|
||||
specifier: ^3.5.0
|
||||
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-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':
|
||||
resolution: {integrity: sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==}
|
||||
engines: {node: '>=16.0.0 || 14 >= 14.17'}
|
||||
|
|
@ -2638,6 +2673,10 @@ packages:
|
|||
function-bind@1.1.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
|
||||
|
||||
|
|
@ -2872,6 +2911,10 @@ packages:
|
|||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-1IynUYEc/HAwxhi3WDpIpxJbZpMCvvrrmZVqvj9EhpvbH8lls7HhdhiByjL7DkAaWlLIzpC0Xc/VPvy/UxLNjA==}
|
||||
|
||||
|
|
@ -3494,10 +3537,17 @@ packages:
|
|||
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
redis-om@0.4.6:
|
||||
resolution: {integrity: sha512-L6cfZfG+I7ES+hHfBBKQwUEbfmGQJhIvcreP5NgxkxX+LtYLLXrcpP/sIIW1jMyjdgJE1KRjAbiyiuL2AAHe3g==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
redis-parser@3.0.0:
|
||||
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
redis@4.7.0:
|
||||
resolution: {integrity: sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==}
|
||||
|
||||
reflect-metadata@0.2.2:
|
||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||
|
||||
|
|
@ -3883,6 +3933,10 @@ packages:
|
|||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
ulid@2.3.0:
|
||||
resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.13.0:
|
||||
resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==}
|
||||
|
||||
|
|
@ -4024,6 +4078,9 @@ packages:
|
|||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
yaml@1.10.2:
|
||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
|
@ -5252,6 +5309,32 @@ snapshots:
|
|||
react-dom: 18.3.1(react@18.3.1)
|
||||
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)':
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.0(rollup@4.20.0)
|
||||
|
|
@ -6690,6 +6773,8 @@ snapshots:
|
|||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
generic-pool@3.9.0: {}
|
||||
|
||||
get-func-name@2.0.2: {}
|
||||
|
||||
get-intrinsic@1.2.4:
|
||||
|
|
@ -6945,6 +7030,8 @@ snapshots:
|
|||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
jsonpath-plus@7.2.0: {}
|
||||
|
||||
just-clone@6.2.0: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
|
|
@ -7428,10 +7515,26 @@ snapshots:
|
|||
|
||||
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:
|
||||
dependencies:
|
||||
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: {}
|
||||
|
||||
regenerator-runtime@0.14.1:
|
||||
|
|
@ -7870,6 +7973,8 @@ snapshots:
|
|||
|
||||
typescript@5.5.4: {}
|
||||
|
||||
ulid@2.3.0: {}
|
||||
|
||||
undici-types@6.13.0: {}
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
|
@ -7995,6 +8100,8 @@ snapshots:
|
|||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
yaml@1.10.2: {}
|
||||
|
||||
yaml@2.5.0: {}
|
||||
|
|
|
|||
68
render.yaml
68
render.yaml
|
|
@ -1,4 +1,5 @@
|
|||
services:
|
||||
# Web
|
||||
- type: web
|
||||
name: web
|
||||
runtime: node
|
||||
|
|
@ -9,6 +10,21 @@ services:
|
|||
startCommand: npm run db:migrate && node build/index.js
|
||||
healthCheckPath: /
|
||||
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
|
||||
fromDatabase:
|
||||
name: tofustack
|
||||
|
|
@ -23,10 +39,62 @@ services:
|
|||
name: web
|
||||
type: web
|
||||
property: host
|
||||
# Redis
|
||||
- type: redis
|
||||
name: redis
|
||||
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:
|
||||
- name: db
|
||||
databaseName: tofustack
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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>;
|
||||
22
src/lib/server/api/common/config.ts
Normal file
22
src/lib/server/api/common/config.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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()
|
||||
// }
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import type { DatabaseProvider } from "../../providers/database.provider";
|
||||
|
||||
export interface Repository {
|
||||
trxHost(trx: DatabaseProvider): any;
|
||||
}
|
||||
3
src/lib/server/api/common/types/async-service.ts
Normal file
3
src/lib/server/api/common/types/async-service.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export abstract class AsyncService {
|
||||
async init(): Promise<void> { }
|
||||
}
|
||||
26
src/lib/server/api/common/types/config.type.ts
Normal file
26
src/lib/server/api/common/types/config.type.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Hono } from "hono";
|
||||
import type { HonoTypes } from "../types/hono.type";
|
||||
import type { BlankSchema, Env, Schema } from "hono/types";
|
||||
import type { HonoTypes } from "./hono";
|
||||
import type { BlankSchema } from "hono/types";
|
||||
|
||||
export abstract class Controler {
|
||||
protected readonly controller: Hono<HonoTypes, BlankSchema, '/'>;
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
|
||||
|
||||
|
||||
export interface Email {
|
||||
subject(): string
|
||||
html(): string;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import * as envs from '$env/static/private';
|
||||
|
||||
export const env = { ...envs, isProduction: process.env.NODE_ENV === 'production' };
|
||||
|
|
@ -1,22 +1,21 @@
|
|||
import { Hono, type Schema } from 'hono';
|
||||
import { setCookie } from 'hono/cookie';
|
||||
import { inject, injectable } from 'tsyringe';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
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 { 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()
|
||||
export class IamController extends Controler {
|
||||
export class IamController extends Controler {
|
||||
constructor(
|
||||
@inject(IamService) private iamService: IamService,
|
||||
@inject(LuciaProvider) private lucia: LuciaProvider,
|
||||
@inject(LuciaService) private luciaService: LuciaService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
|
@ -35,7 +34,7 @@ export class IamController extends Controler {
|
|||
.post('/login/verify', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const { email, token } = c.req.valid('json');
|
||||
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, {
|
||||
path: sessionCookie.attributes.path,
|
||||
maxAge: sessionCookie.attributes.maxAge,
|
||||
|
|
@ -50,7 +49,7 @@ export class IamController extends Controler {
|
|||
.post('/logout', requireAuth, async (c) => {
|
||||
const sessionId = c.var.session.id;
|
||||
await this.iamService.logout(sessionId);
|
||||
const sessionCookie = this.lucia.createBlankSessionCookie();
|
||||
const sessionCookie = this.luciaService.lucia.createBlankSessionCookie();
|
||||
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||
path: sessionCookie.attributes.path,
|
||||
maxAge: sessionCookie.attributes.maxAge,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { createId } from '@paralleldrive/cuid2';
|
|||
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
import { usersTable } from './users.table';
|
||||
import { timestamps } from '../../common/utils/table.utils';
|
||||
import { timestamps } from '../../../common/utils/table';
|
||||
|
||||
export const emailVerificationsTable = pgTable('email_verifications', {
|
||||
id: text('id')
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
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";
|
||||
|
||||
export const filesTable = pgTable('files', {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { relations } from 'drizzle-orm';
|
||||
import { createId } from '@paralleldrive/cuid2';
|
||||
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', {
|
||||
id: text('id')
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { cuid2 } from '../../common/utils/table.utils';
|
||||
import { cuid2 } from '../../../common/utils/table';
|
||||
import { usersTable } from './users.table';
|
||||
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ import { createId } from '@paralleldrive/cuid2';
|
|||
import { sessionsTable } from './sessions.table';
|
||||
import { boolean, pgTable, text } from 'drizzle-orm/pg-core';
|
||||
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';
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { Schema } from 'redis-om'
|
||||
|
||||
export const loginRequestSchema = new Schema('album', {
|
||||
id: { type: 'string' },
|
||||
hashedToken: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
})
|
||||
|
|
@ -3,4 +3,5 @@ import { z } from 'zod';
|
|||
export const verifyEmailDto = z.object({
|
||||
token: z.string()
|
||||
});
|
||||
|
||||
export type VerifyEmailDto = z.infer<typeof verifyEmailDto>;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Email } from "../common/inferfaces/email.interface"
|
||||
import type { Email } from "../common/types/email"
|
||||
|
||||
export class EmailChangeNoticeEmail implements Email {
|
||||
constructor() { }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Email } from "../common/inferfaces/email.interface"
|
||||
import type { Email } from "../common/types/email"
|
||||
|
||||
export class LoginVerificationEmail implements Email {
|
||||
constructor(private readonly token: string) { }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Email } from "../common/inferfaces/email.interface";
|
||||
import type { Email } from "../common/types/email";
|
||||
|
||||
export class WelcomeEmail implements Email {
|
||||
constructor() { }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Hono } from 'hono';
|
|||
import { hc } from 'hono/client';
|
||||
import { container } from 'tsyringe';
|
||||
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 { AuthCleanupJobs } from './jobs/auth-cleanup.job';
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ container.resolve(AuthCleanupJobs).deleteStaleLoginRequests();
|
|||
/* -------------------------------------------------------------------------- */
|
||||
/* Exports */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
const rpc = hc<typeof routes>(env.ORIGIN);
|
||||
const rpc = hc<typeof routes>(config.api.origin);
|
||||
export type ApiClient = typeof rpc;
|
||||
export type ApiRoutes = typeof routes;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { inject, injectable } from "tsyringe";
|
||||
import { JobsService } from "../services/jobs.service";
|
||||
|
||||
// Example on how to create a job that runs once a week at midnight on Sunday
|
||||
|
||||
@injectable()
|
||||
export class AuthCleanupJobs {
|
||||
private queue;
|
||||
|
||||
constructor(
|
||||
@inject(JobsService) private jobsService: JobsService,
|
||||
) {
|
||||
/* ------------------------------ Create Queue ------------------------------ */
|
||||
this.queue = this.jobsService.createQueue('test')
|
||||
constructor(@inject(JobsService) private jobsService: JobsService) {
|
||||
// create queue
|
||||
this.queue = this.jobsService.createQueue('auth_cleanup')
|
||||
|
||||
/* ---------------------------- Register Workers ---------------------------- */
|
||||
// register workers
|
||||
this.worker();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,14 @@ import { createMiddleware } from 'hono/factory';
|
|||
import { verifyRequestOrigin } from 'lucia';
|
||||
import type { Session, User } from 'lucia';
|
||||
import { Unauthorized } from '../common/exceptions';
|
||||
import type { HonoTypes } from '../common/types/hono.type';
|
||||
import { lucia } from '../packages/lucia';
|
||||
import type { HonoTypes } from '../common/types/hono';
|
||||
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) => {
|
||||
if (c.req.method === "GET") {
|
||||
return next();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { rateLimiter } from "hono-rate-limiter";
|
||||
import { RedisStore } from 'rate-limit-redis'
|
||||
import RedisClient from 'ioredis'
|
||||
import { env } from "../configs/envs.config";
|
||||
import type { HonoTypes } from "../common/types/hono.type";
|
||||
import type { HonoTypes } from "../common/types/hono";
|
||||
import { container } from "tsyringe";
|
||||
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 = "" }: {
|
||||
limit: number;
|
||||
minutes: number;
|
||||
|
|
@ -23,8 +25,7 @@ export function limiter({ limit, minutes, key = "" }: {
|
|||
}, // Method to generate custom identifiers for clients.
|
||||
// Redis store configuration
|
||||
store: new RedisStore({
|
||||
// @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis
|
||||
sendCommand: (...args: string[]) => client.call(...args),
|
||||
sendCommand: (...args: string[]) => client.sendCommand(args),
|
||||
}) as any,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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 });
|
||||
|
|
@ -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 });
|
||||
|
|
@ -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
|
||||
})
|
||||
});
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
@ -1,39 +1,35 @@
|
|||
import { inject, injectable } from "tsyringe";
|
||||
import { and, eq, gte, lte, type InferInsertModel } from "drizzle-orm";
|
||||
import { emailVerificationsTable } from "../databases/tables";
|
||||
import type { Repository } from "../common/inferfaces/repository.interface";
|
||||
import { DatabaseProvider } from "../providers/database.provider";
|
||||
import { takeFirst, takeFirstOrThrow } from "../common/utils/repository.utils";
|
||||
|
||||
import { and, eq, gte, type InferInsertModel } from "drizzle-orm";
|
||||
import { emailVerificationsTable } from "../databases/postgres/tables";
|
||||
import { takeFirst, takeFirstOrThrow } from "../common/utils/repository";
|
||||
import { DrizzleService } from "../services/drizzle.service";
|
||||
|
||||
export type CreateEmailVerification = Pick<InferInsertModel<typeof emailVerificationsTable>, 'requestedEmail' | 'hashedToken' | 'userId' | 'expiresAt'>;
|
||||
|
||||
@injectable()
|
||||
export class EmailVerificationsRepository implements Repository {
|
||||
constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) { }
|
||||
export class EmailVerificationsRepository {
|
||||
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) { }
|
||||
|
||||
// creates a new email verification record or updates an existing one
|
||||
async create(data: CreateEmailVerification) {
|
||||
return this.db.insert(emailVerificationsTable).values(data).onConflictDoUpdate({
|
||||
return this.drizzle.db.insert(emailVerificationsTable).values(data).onConflictDoUpdate({
|
||||
target: emailVerificationsTable.userId,
|
||||
set: data
|
||||
}).returning().then(takeFirstOrThrow)
|
||||
}
|
||||
|
||||
// finds a valid record by token and userId
|
||||
async findValidRecord(userId: string) {
|
||||
return this.db.select().from(emailVerificationsTable).where(
|
||||
async findValidRecord(userId: string, db = this.drizzle.db) {
|
||||
return db.select().from(emailVerificationsTable).where(
|
||||
and(
|
||||
eq(emailVerificationsTable.userId, userId),
|
||||
gte(emailVerificationsTable.expiresAt, new Date())
|
||||
)).then(takeFirst)
|
||||
}
|
||||
|
||||
async deleteById(id: string) {
|
||||
return this.db.delete(emailVerificationsTable).where(eq(emailVerificationsTable.id, id))
|
||||
async deleteById(id: string, db = this.drizzle.db) {
|
||||
return db.delete(emailVerificationsTable).where(eq(emailVerificationsTable.id, id))
|
||||
}
|
||||
|
||||
trxHost(trx: DatabaseProvider) {
|
||||
return new EmailVerificationsRepository(trx)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,29 +1,29 @@
|
|||
import { inject } from "tsyringe";
|
||||
import { StorageService } from "../services/storage.service";
|
||||
import { DatabaseProvider } from "../providers/database.provider";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { filesTable } from "../databases/tables/files.table";
|
||||
import { takeFirst, takeFirstOrThrow } from "../common/utils/repository.utils";
|
||||
import { filesTable } from "../databases/postgres/tables/files.table";
|
||||
import { takeFirst, takeFirstOrThrow } from "../common/utils/repository";
|
||||
import { DrizzleService } from "../services/drizzle.service";
|
||||
|
||||
export class FilesRepository {
|
||||
constructor(
|
||||
@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);
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
async update(id: string, file: File, db = this.db) {
|
||||
async update(id: string, file: File, db = this.drizzle.db) {
|
||||
// upload new 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))
|
||||
|
|
@ -33,7 +33,7 @@ export class FilesRepository {
|
|||
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)
|
||||
await this.storageService.delete(asset.key)
|
||||
await db.delete(filesTable).where(eq(filesTable.id, id))
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
import { inject, injectable } from "tsyringe";
|
||||
import { and, eq, gte, type InferInsertModel } from "drizzle-orm";
|
||||
import { loginRequestsTable } from "../databases/tables";
|
||||
import type { Repository } from "../common/inferfaces/repository.interface";
|
||||
import { DatabaseProvider } from "../providers/database.provider";
|
||||
import { takeFirst, takeFirstOrThrow } from "../common/utils/repository.utils";
|
||||
import { loginRequestsTable } from "../databases/postgres/tables";
|
||||
import { takeFirst, takeFirstOrThrow } from "../common/utils/repository";
|
||||
import { DrizzleService } from "../services/drizzle.service";
|
||||
|
||||
|
||||
export type CreateLoginRequest = Pick<InferInsertModel<typeof loginRequestsTable>, 'email' | 'expiresAt' | 'hashedToken'>;
|
||||
|
||||
@injectable()
|
||||
export class LoginRequestsRepository implements Repository {
|
||||
constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) { }
|
||||
export class LoginRequestsRepository {
|
||||
constructor(
|
||||
@inject(DrizzleService) private readonly drizzle: DrizzleService,
|
||||
) { }
|
||||
|
||||
async create(data: CreateLoginRequest) {
|
||||
return this.db.insert(loginRequestsTable).values(data).onConflictDoUpdate({
|
||||
async create(data: CreateLoginRequest, db = this.drizzle.db) {
|
||||
return db.insert(loginRequestsTable).values(data).onConflictDoUpdate({
|
||||
target: loginRequestsTable.email,
|
||||
set: data
|
||||
}).returning().then(takeFirstOrThrow)
|
||||
}
|
||||
|
||||
async findOneByEmail(email: string) {
|
||||
return this.db.select().from(loginRequestsTable).where(
|
||||
async findOneByEmail(email: string, db = this.drizzle.db) {
|
||||
return db.select().from(loginRequestsTable).where(
|
||||
and(
|
||||
eq(loginRequestsTable.email, email),
|
||||
gte(loginRequestsTable.expiresAt, new Date())
|
||||
|
|
@ -28,11 +29,7 @@ export class LoginRequestsRepository implements Repository {
|
|||
).then(takeFirst)
|
||||
}
|
||||
|
||||
async deleteById(id: string) {
|
||||
return this.db.delete(loginRequestsTable).where(eq(loginRequestsTable.id, id));
|
||||
}
|
||||
|
||||
trxHost(trx: DatabaseProvider) {
|
||||
return new LoginRequestsRepository(trx);
|
||||
async deleteById(id: string, db = this.drizzle.db) {
|
||||
return db.delete(loginRequestsTable).where(eq(loginRequestsTable.id, id));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +1,45 @@
|
|||
import { inject, injectable } from 'tsyringe';
|
||||
import { usersTable } from '../databases/tables';
|
||||
import { usersTable } from '../databases/postgres/tables';
|
||||
import { eq, type InferInsertModel } from 'drizzle-orm';
|
||||
import { DatabaseProvider } from '../providers/database.provider';
|
||||
import { takeFirstOrThrow } from '../common/utils/repository.utils';
|
||||
import { takeFirstOrThrow } from '../common/utils/repository';
|
||||
import type { Repository } from '../common/inferfaces/repository.interface';
|
||||
import { DrizzleService } from '../services/drizzle.service';
|
||||
|
||||
export type CreateUser = InferInsertModel<typeof usersTable>;
|
||||
export type UpdateUser = Partial<CreateUser>;
|
||||
|
||||
@injectable()
|
||||
export class UsersRepository implements Repository {
|
||||
constructor(@inject(DatabaseProvider) private db: DatabaseProvider) { }
|
||||
export class UsersRepository {
|
||||
constructor(@inject(DrizzleService) private drizzle: DrizzleService) { }
|
||||
|
||||
async findOneById(id: string) {
|
||||
return this.db.query.usersTable.findFirst({
|
||||
async findOneById(id: string, db = this.drizzle.db) {
|
||||
return db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.id, id)
|
||||
});
|
||||
}
|
||||
|
||||
async findOneByIdOrThrow(id: string) {
|
||||
const user = await this.findOneById(id);
|
||||
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
|
||||
const user = await this.findOneById(id, db);
|
||||
if (!user) throw Error('User not found');
|
||||
return user;
|
||||
}
|
||||
|
||||
async findOneByEmail(email: string) {
|
||||
return this.db.query.usersTable.findFirst({
|
||||
async findOneByEmail(email: string, db = this.drizzle.db) {
|
||||
return db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.email, email)
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: CreateUser) {
|
||||
return this.db.insert(usersTable).values(data).returning().then(takeFirstOrThrow);
|
||||
async create(data: CreateUser, db = this.drizzle.db) {
|
||||
return db.insert(usersTable).values(data).returning().then(takeFirstOrThrow);
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateUser) {
|
||||
return this.db
|
||||
async update(id: string, data: UpdateUser, db = this.drizzle.db) {
|
||||
return db
|
||||
.update(usersTable)
|
||||
.set(data)
|
||||
.where(eq(usersTable.id, id))
|
||||
.returning()
|
||||
.then(takeFirstOrThrow);
|
||||
}
|
||||
|
||||
trxHost(trx: DatabaseProvider) {
|
||||
return new UsersRepository(trx);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
src/lib/server/api/services/drizzle.service.ts
Normal file
22
src/lib/server/api/services/drizzle.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
import { inject, injectable } from 'tsyringe';
|
||||
import { MailerService } from './mailer.service';
|
||||
import { TokensService } from './tokens.service';
|
||||
import { LuciaProvider } from '../providers/lucia.provider';
|
||||
import { UsersRepository } from '../repositories/users.repository';
|
||||
import type { SignInEmailDto } from '../dtos/signin-email.dto';
|
||||
import type { RegisterEmailDto } from '../dtos/register-email.dto';
|
||||
import { LoginRequestsRepository } from '../repositories/login-requests.repository';
|
||||
import { LoginVerificationEmail } from '../emails/login-verification.email';
|
||||
import { DatabaseProvider } from '../providers/database.provider';
|
||||
import { BadRequest } from '../common/exceptions';
|
||||
import { WelcomeEmail } from '../emails/welcome.email';
|
||||
import { EmailVerificationsRepository } from '../repositories/email-verifications.repository';
|
||||
import { EmailChangeNoticeEmail } from '../emails/email-change-notice.email';
|
||||
import { DrizzleService } from './drizzle.service';
|
||||
import { LuciaService } from './lucia.service';
|
||||
|
||||
@injectable()
|
||||
export class IamService {
|
||||
constructor(
|
||||
@inject(LuciaProvider) private readonly lucia: LuciaProvider,
|
||||
@inject(DatabaseProvider) private readonly db: DatabaseProvider,
|
||||
@inject(LuciaService) private readonly luciaService: LuciaService,
|
||||
@inject(DrizzleService) private readonly drizzleService: DrizzleService,
|
||||
@inject(TokensService) private readonly tokensService: TokensService,
|
||||
@inject(MailerService) private readonly mailerService: MailerService,
|
||||
@inject(UsersRepository) private readonly usersRepository: UsersRepository,
|
||||
|
|
@ -42,10 +42,10 @@ export class IamService {
|
|||
|
||||
if (!existingUser) {
|
||||
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.
|
||||
|
|
@ -80,7 +80,7 @@ export class IamService {
|
|||
}
|
||||
|
||||
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
|
||||
|
|
@ -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
|
||||
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
|
||||
const loginRequest = await this.loginRequestsRepository.trxHost(trx).findOneByEmail(email)
|
||||
const loginRequest = await this.loginRequestsRepository.findOneByEmail(email, trx)
|
||||
if (!loginRequest) return null;
|
||||
|
||||
// check if the token is valid
|
||||
|
|
@ -103,15 +103,15 @@ export class IamService {
|
|||
if (!isValidRequest) return null
|
||||
|
||||
// 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
|
||||
})
|
||||
}
|
||||
|
||||
private async findAndBurnEmailVerificationToken(userId: string, token: string) {
|
||||
return this.db.transaction(async (trx) => {
|
||||
return this.drizzleService.db.transaction(async (trx) => {
|
||||
// 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;
|
||||
|
||||
// check if the token is valid
|
||||
|
|
@ -119,7 +119,7 @@ export class IamService {
|
|||
if (!isValidRecord) return null
|
||||
|
||||
// burn the token if it is valid
|
||||
await this.emailVerificationsRepository.trxHost(trx).deleteById(emailVerificationRecord.id)
|
||||
await this.emailVerificationsRepository.deleteById(emailVerificationRecord.id, trx)
|
||||
return emailVerificationRecord
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,28 @@
|
|||
import { inject, injectable } from "tsyringe";
|
||||
import { injectable } from "tsyringe";
|
||||
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()
|
||||
export class JobsService {
|
||||
constructor(@inject(RedisProvider) private readonly redis: RedisProvider) {
|
||||
}
|
||||
constructor() { }
|
||||
|
||||
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) {
|
||||
return new Worker(name, prcoessor, { connection: this.redis })
|
||||
return new Worker(name, prcoessor, {
|
||||
connection: new RedisClient(config.redis.url, {
|
||||
maxRetriesPerRequest: null
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
src/lib/server/api/services/lucia.service.ts
Normal file
25
src/lib/server/api/services/lucia.service.ts
Normal 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
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { injectable } from 'tsyringe';
|
||||
import { env } from '../configs/envs.config';
|
||||
import type { Email } from '../common/inferfaces/email.interface';
|
||||
import { config } from '../common/config';
|
||||
|
||||
type SendProps = {
|
||||
to: string | string[];
|
||||
|
|
@ -11,7 +11,7 @@ type SendProps = {
|
|||
export class MailerService {
|
||||
|
||||
async send(data: SendProps) {
|
||||
const mailer = env.isProduction ? this.sendProd : this.sendDev;
|
||||
const mailer = config.isProduction ? this.sendProd : this.sendDev;
|
||||
await mailer(data);
|
||||
}
|
||||
|
||||
|
|
|
|||
42
src/lib/server/api/services/redis.service.ts
Normal file
42
src/lib/server/api/services/redis.service.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 { inject, injectable } from 'tsyringe';
|
||||
import { env } from '../configs/envs.config';
|
||||
import { S3ClientProvider } from '../providers/s3.provider';
|
||||
import { injectable } from 'tsyringe';
|
||||
import { config } from '../common/config';
|
||||
|
||||
@injectable()
|
||||
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) {
|
||||
const key = createId();
|
||||
const uploadCommand = new PutObjectCommand({
|
||||
Bucket: env.STORAGE_BUCKET_NAME,
|
||||
Bucket: config.storage.bucket,
|
||||
ACL: 'public-read',
|
||||
Key: key,
|
||||
ContentType: file.type,
|
||||
|
|
@ -24,7 +35,7 @@ export class StorageService {
|
|||
|
||||
delete(key: string) {
|
||||
const deleteCommand = new DeleteObjectCommand({
|
||||
Bucket: env.STORAGE_BUCKET_NAME,
|
||||
Bucket: config.storage.bucket,
|
||||
Key: key
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
import 'reflect-metadata';
|
||||
import { LoginRequestsService } from '../services/login-requests.service';
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { TokensService } from '../services/tokens.service';
|
||||
import { MailerService } from '../services/mailer.service';
|
||||
import { UsersRepository } from '../repositories/users.repository';
|
||||
import { LoginRequestsRepository } from '../repositories/login-requests.repository';
|
||||
import { PgDatabase } from 'drizzle-orm/pg-core';
|
||||
import { container } from 'tsyringe';
|
||||
import { LuciaProvider } from '../providers/lucia.provider';
|
||||
import { DatabaseProvider } from '../providers/database.provider';
|
||||
import { LuciaService } from '../services/lucia.service';
|
||||
import { DrizzleService } from '../services/drizzle.service';
|
||||
import { IamService } from '../services/iam.service';
|
||||
|
||||
describe('LoginRequestService', () => {
|
||||
let service: LoginRequestsService;
|
||||
let service: IamService;
|
||||
let tokensService = vi.mocked(TokensService.prototype)
|
||||
let mailerService = vi.mocked(MailerService.prototype);
|
||||
let usersRepository = vi.mocked(UsersRepository.prototype);
|
||||
let loginRequestsRepository = vi.mocked(LoginRequestsRepository.prototype);
|
||||
let luciaProvider = vi.mocked(LuciaProvider);
|
||||
let databaseProvider = vi.mocked(PgDatabase);
|
||||
let luciaService = vi.mocked(LuciaService.prototype);
|
||||
let drizzleService = vi.mocked(DrizzleService.prototype);
|
||||
|
||||
beforeAll(() => {
|
||||
service = container
|
||||
|
|
@ -25,9 +24,9 @@ describe('LoginRequestService', () => {
|
|||
.register<MailerService>(MailerService, { useValue: mailerService })
|
||||
.register<UsersRepository>(UsersRepository, { useValue: usersRepository })
|
||||
.register(LoginRequestsRepository, { useValue: loginRequestsRepository })
|
||||
.register(LuciaProvider, { useValue: luciaProvider })
|
||||
.register(DatabaseProvider, { useValue: databaseProvider })
|
||||
.resolve(LoginRequestsService);
|
||||
.register(LuciaService, { useValue: luciaService })
|
||||
.register(DrizzleService, { useValue: drizzleService })
|
||||
.resolve(IamService);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
|
|
@ -57,7 +56,7 @@ describe('LoginRequestService', () => {
|
|||
const spy_loginRequestsRepository_create = vi.spyOn(loginRequestsRepository, 'create')
|
||||
|
||||
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 () => {
|
||||
expect(spy_tokensService_generateTokenWithExpiryAndHash).toBeCalledTimes(1)
|
||||
|
|
|
|||
|
|
@ -1,29 +1,28 @@
|
|||
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 { StatusCodes } from "$lib/constants/status-codes.js";
|
||||
import { updateEmailFormSchema, verifyEmailFormSchema } from "./schemas.js";
|
||||
|
||||
export let load = async (event) => {
|
||||
const authedUser = await event.locals.getAuthedUserOrThrow()
|
||||
|
||||
return {
|
||||
authedUser,
|
||||
updateEmailForm: await superValidate(authedUser, zod(updateEmailDto)),
|
||||
verifyEmailForm: await superValidate(zod(verifyEmailDto))
|
||||
updateEmailForm: await superValidate(authedUser, zod(updateEmailFormSchema)),
|
||||
verifyEmailForm: await superValidate(zod(verifyEmailFormSchema))
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
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 })
|
||||
const { error } = await locals.api.iam.email.$patch({ json: updateEmailForm.data }).then(locals.parseApiResponse);
|
||||
if (error) return setError(updateEmailForm, 'email', error);
|
||||
return { updateEmailForm }
|
||||
},
|
||||
verifyEmail: async ({ request, locals }) => {
|
||||
const verifyEmailForm = await superValidate(request, zod(verifyEmailDto));
|
||||
const verifyEmailForm = await superValidate(request, zod(verifyEmailFormSchema));
|
||||
console.log(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);
|
||||
|
|
|
|||
9
src/routes/(app)/settings/account/schemas.ts
Normal file
9
src/routes/(app)/settings/account/schemas.ts
Normal 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()
|
||||
});
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
<script context="module" lang="ts">
|
||||
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 {
|
||||
updateEmailForm: SuperValidated<Infer<typeof updateEmailDto>>;
|
||||
|
|
@ -16,6 +14,8 @@
|
|||
import { superForm } from 'sveltekit-superforms';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
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 --------------------------------- */
|
||||
let { updateEmailForm, verifyEmailForm }: UpdateEmailCardProps = $props();
|
||||
|
|
|
|||
|
|
@ -1,27 +1,26 @@
|
|||
import { fail, redirect } from '@sveltejs/kit';
|
||||
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 { setError, superValidate } from 'sveltekit-superforms';
|
||||
import { registerFormSchema, signInFormSchema } from './schemas';
|
||||
|
||||
export const load = async () => {
|
||||
return {
|
||||
emailRegisterForm: await superValidate(zod(registerEmailDto)),
|
||||
emailSigninForm: await superValidate(zod(signInEmailDto))
|
||||
emailRegisterForm: await superValidate(zod(registerFormSchema)),
|
||||
emailSigninForm: await superValidate({email: 'test'}, zod(signInFormSchema))
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
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 });
|
||||
const { error } = await locals.api.iam.login.request.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse);
|
||||
if (error) return setError(emailRegisterForm, 'email', error);
|
||||
return { emailRegisterForm };
|
||||
},
|
||||
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 });
|
||||
const { error } = await locals.api.iam.login.verify.$post({ json: emailSignInForm.data }).then(locals.parseApiResponse)
|
||||
if (error) return setError(emailSignInForm, 'token', error);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import * as Card from "$lib/components/ui/card/index.js";
|
||||
import { Input } from "$lib/components/ui/input/index.js";
|
||||
import { Label } from "$lib/components/ui/label/index.js";
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { superForm } from 'sveltekit-superforms';
|
||||
import * as Form from '$lib/components/ui/form';
|
||||
import { zodClient } from 'sveltekit-superforms/adapters';
|
||||
import { registerEmailDto } from "$lib/dtos/register-email.dto.js";
|
||||
import { signInEmailDto } from "$lib/dtos/signin-email.dto.js";
|
||||
import PinInput from "$lib/components/pin-input.svelte";
|
||||
import PinInput from '$lib/components/pin-input.svelte';
|
||||
import { registerFormSchema, signInFormSchema } from './schemas.js';
|
||||
|
||||
const {data} = $props();
|
||||
const { data } = $props();
|
||||
|
||||
let showTokenVerification = $state(false);
|
||||
|
||||
const emailRegisterForm = superForm(data.emailRegisterForm, {
|
||||
validators: zodClient(registerEmailDto),
|
||||
validators: zodClient(registerFormSchema),
|
||||
resetForm: false,
|
||||
onUpdated: ({ form }) => {
|
||||
if (form.valid) {
|
||||
|
|
@ -46,13 +24,12 @@
|
|||
});
|
||||
|
||||
const emailSigninForm = superForm(data.emailSigninForm, {
|
||||
validators: zodClient(signInEmailDto),
|
||||
validators: zodClient(signInFormSchema),
|
||||
resetForm: false
|
||||
});
|
||||
|
||||
const { form: emailRegisterFormData, enhance: emailRegisterEnhance } = emailRegisterForm;
|
||||
const { form: emailSigninFormData, enhance: emailSigninEnhance } = emailSigninForm;
|
||||
|
||||
</script>
|
||||
|
||||
<Card.Root class="mx-auto mt-24 max-w-sm">
|
||||
|
|
|
|||
12
src/routes/(auth)/register/schemas.ts
Normal file
12
src/routes/(auth)/register/schemas.ts
Normal 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()
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in a new issue