mirror of
https://github.com/BradNut/TofuStack
synced 2025-09-08 17:40:26 +00:00
removed providers in favor of services
This commit is contained in:
parent
4f38b4a6d0
commit
70a0c74948
53 changed files with 413 additions and 212 deletions
12
.env.example
12
.env.example
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
107
pnpm-lock.yaml
107
pnpm-lock.yaml
|
|
@ -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: {}
|
||||||
|
|
|
||||||
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,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 { 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, '/'>;
|
||||||
|
|
@ -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 { 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 { 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 { registerEmailDto } from '$lib/server/api/dtos/register-email.dto';
|
||||||
import { signInEmailDto } from '$lib/server/api/dtos/signin-email.dto';
|
import { signInEmailDto } from '$lib/server/api/dtos/signin-email.dto';
|
||||||
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
|
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
|
||||||
import { verifyEmailDto } from '$lib/server/api/dtos/verify-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,
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
@ -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', {
|
||||||
|
|
@ -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')
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Schema } from 'redis-om'
|
||||||
|
|
||||||
|
export const loginRequestSchema = new Schema('album', {
|
||||||
|
id: { type: 'string' },
|
||||||
|
hashedToken: { type: 'string' },
|
||||||
|
email: { type: 'string' },
|
||||||
|
})
|
||||||
|
|
@ -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() { }
|
||||||
|
|
|
||||||
|
|
@ -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) { }
|
||||||
|
|
|
||||||
|
|
@ -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() { }
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ export class AuthCleanupJobs {
|
||||||
private queue;
|
private queue;
|
||||||
|
|
||||||
constructor(@inject(JobsService) private jobsService: JobsService) {
|
constructor(@inject(JobsService) private jobsService: JobsService) {
|
||||||
/* ------------------------------ Create Queue ------------------------------ */
|
// create queue
|
||||||
this.queue = this.jobsService.createQueue('auth_cleanup')
|
this.queue = this.jobsService.createQueue('auth_cleanup')
|
||||||
|
|
||||||
/* ---------------------------- Register Workers ---------------------------- */
|
// register workers
|
||||||
this.worker();
|
this.worker();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 { 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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +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
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 { 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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 { 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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { registerFormSchema, signInFormSchema } from './schemas';
|
||||||
export const load = async () => {
|
export const load = async () => {
|
||||||
return {
|
return {
|
||||||
emailRegisterForm: await superValidate(zod(registerFormSchema)),
|
emailRegisterForm: await superValidate(zod(registerFormSchema)),
|
||||||
emailSigninForm: await superValidate(zod(signInFormSchema))
|
emailSigninForm: await superValidate({email: 'test'}, zod(signInFormSchema))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue