Converting everything to the tsyringe IoC pattern.

This commit is contained in:
Bradley Shellnut 2024-07-30 18:50:46 -07:00
parent 3190e9601e
commit bf55b04de6
19 changed files with 415 additions and 57 deletions

View file

@ -1 +1 @@
20.15.1
22.1.0

View file

@ -43,6 +43,10 @@
"eslint-plugin-svelte": "^2.43.0",
"just-clone": "^6.2.0",
"just-debounce-it": "^3.2.0",
"lucia": "3.2.0",
"lucide-svelte": "^0.408.0",
"nodemailer": "^6.9.14",
"oslo": "^1.2.1",
"postcss": "^8.4.40",
"postcss-import": "^16.1.0",
"postcss-load-config": "^5.1.0",
@ -71,10 +75,6 @@
"zod": "^3.23.8"
},
"type": "module",
"engines": {
"node": ">=18.0.0 <19.0.0 || >=20.0.0 <21.0.0",
"pnpm": ">=8"
},
"dependencies": {
"@fontsource/fira-mono": "^5.0.13",
"@hono/zod-validator": "^0.2.2",
@ -91,6 +91,7 @@
"arctic": "^1.9.2",
"bits-ui": "^0.21.12",
"boardgamegeekclient": "^1.9.1",
"bullmq": "^5.11.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cookie": "^0.6.0",
@ -99,6 +100,7 @@
"drizzle-orm": "^0.32.1",
"feather-icons": "^4.29.2",
"formsnap": "^1.0.1",
"handlebars": "^4.7.8",
"hono": "^4.5.2",
"hono-rate-limiter": "^0.4.0",
"html-entities": "^2.5.2",
@ -107,10 +109,7 @@
"just-capitalize": "^3.2.0",
"just-kebab-case": "^4.2.0",
"loader": "^2.1.1",
"lucia": "3.2.0",
"lucide-svelte": "^0.408.0",
"open-props": "^1.7.5",
"oslo": "^1.2.1",
"pg": "^8.12.0",
"postgres": "^3.4.4",
"qrcode": "^1.5.3",

View file

@ -53,6 +53,9 @@ importers:
boardgamegeekclient:
specifier: ^1.9.1
version: 1.9.1
bullmq:
specifier: ^5.11.0
version: 5.11.0
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
@ -77,6 +80,9 @@ importers:
formsnap:
specifier: ^1.0.1
version: 1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.16.1(@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.175)(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8)))(svelte@5.0.0-next.175)(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8)))(svelte@5.0.0-next.175))
handlebars:
specifier: ^4.7.8
version: 4.7.8
hono:
specifier: ^4.5.2
version: 4.5.2
@ -101,18 +107,9 @@ importers:
loader:
specifier: ^2.1.1
version: 2.1.1
lucia:
specifier: 3.2.0
version: 3.2.0
lucide-svelte:
specifier: ^0.408.0
version: 0.408.0(svelte@5.0.0-next.175)
open-props:
specifier: ^1.7.5
version: 1.7.5
oslo:
specifier: ^1.2.1
version: 1.2.1
pg:
specifier: ^8.12.0
version: 8.12.0
@ -210,6 +207,18 @@ importers:
just-debounce-it:
specifier: ^3.2.0
version: 3.2.0
lucia:
specifier: 3.2.0
version: 3.2.0
lucide-svelte:
specifier: ^0.408.0
version: 0.408.0(svelte@5.0.0-next.175)
nodemailer:
specifier: ^6.9.14
version: 6.9.14
oslo:
specifier: ^1.2.1
version: 1.2.1
postcss:
specifier: ^8.4.40
version: 8.4.40
@ -1386,6 +1395,36 @@ packages:
peerDependencies:
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.118
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==}
cpu: [arm64]
os: [darwin]
'@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3':
resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==}
cpu: [x64]
os: [darwin]
'@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3':
resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==}
cpu: [arm64]
os: [linux]
'@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3':
resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==}
cpu: [arm]
os: [linux]
'@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3':
resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==}
cpu: [x64]
os: [linux]
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==}
cpu: [x64]
os: [win32]
'@neondatabase/serverless@0.9.4':
resolution: {integrity: sha512-D0AXgJh6xkf+XTlsO7iwE2Q1w8981E1cLCPAALMU2YKtkF/1SF6BiAzYARZFYo175ON+b1RNIy9TdSFHm5nteg==}
@ -2225,6 +2264,9 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
bullmq@5.11.0:
resolution: {integrity: sha512-qVzyWGZqie3VHaYEgRXhId/j8ebfmj6MExEJyUByMsUJA5pVciVle3hKLer5fyMwtQ8lTMP7GwhXV/NZ+HzlRA==}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@ -2357,6 +2399,10 @@ packages:
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
cron-parser@4.9.0:
resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==}
engines: {node: '>=12.0.0'}
cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@ -2943,6 +2989,11 @@ packages:
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
handlebars@4.7.8:
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
engines: {node: '>=0.4.7'}
hasBin: true
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
@ -3205,6 +3256,10 @@ packages:
peerDependencies:
svelte: ^3 || ^4 || ^5.0.0-next.42
luxon@3.4.4:
resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==}
engines: {node: '>=12'}
magic-string@0.30.10:
resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==}
@ -3338,6 +3393,13 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
msgpackr-extract@3.0.3:
resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==}
hasBin: true
msgpackr@1.11.0:
resolution: {integrity: sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
@ -3358,6 +3420,12 @@ packages:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
node-abort-controller@3.1.1:
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
@ -3367,6 +3435,10 @@ packages:
encoding:
optional: true
node-gyp-build-optional-packages@5.2.2:
resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
hasBin: true
node-gyp-build@4.8.1:
resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==}
hasBin: true
@ -3374,6 +3446,10 @@ packages:
node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
nodemailer@6.9.14:
resolution: {integrity: sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==}
engines: {node: '>=6.0.0'}
nopt@5.0.0:
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
engines: {node: '>=6'}
@ -4519,6 +4595,11 @@ packages:
ufo@1.5.3:
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
uglify-js@3.19.1:
resolution: {integrity: sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==}
engines: {node: '>=0.8.0'}
hasBin: true
ultrahtml@1.5.3:
resolution: {integrity: sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==}
@ -4557,6 +4638,10 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
@ -4670,6 +4755,9 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
wordwrap@1.0.0:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
@ -5579,6 +5667,24 @@ snapshots:
nanoid: 5.0.7
svelte: 5.0.0-next.175
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
optional: true
'@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3':
optional: true
'@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3':
optional: true
'@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3':
optional: true
'@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3':
optional: true
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
optional: true
'@neondatabase/serverless@0.9.4':
dependencies:
'@types/pg': 8.11.6
@ -6367,6 +6473,18 @@ snapshots:
buffer-from@1.1.2: {}
bullmq@5.11.0:
dependencies:
cron-parser: 4.9.0
ioredis: 5.4.1
msgpackr: 1.11.0
node-abort-controller: 3.1.1
semver: 7.6.3
tslib: 2.6.3
uuid: 9.0.1
transitivePeerDependencies:
- supports-color
bytes@3.1.2: {}
cac@6.7.14: {}
@ -6493,6 +6611,10 @@ snapshots:
create-require@1.1.1: {}
cron-parser@4.9.0:
dependencies:
luxon: 3.4.4
cross-spawn@7.0.3:
dependencies:
path-key: 3.1.1
@ -7127,6 +7249,15 @@ snapshots:
graphemer@1.4.0: {}
handlebars@4.7.8:
dependencies:
minimist: 1.2.8
neo-async: 2.6.2
source-map: 0.6.1
wordwrap: 1.0.0
optionalDependencies:
uglify-js: 3.19.1
has-flag@4.0.0: {}
has-property-descriptors@1.0.2:
@ -7373,6 +7504,8 @@ snapshots:
dependencies:
svelte: 5.0.0-next.175
luxon@3.4.4: {}
magic-string@0.30.10:
dependencies:
'@jridgewell/sourcemap-codec': 1.4.15
@ -7481,6 +7614,22 @@ snapshots:
ms@2.1.3: {}
msgpackr-extract@3.0.3:
dependencies:
node-gyp-build-optional-packages: 5.2.2
optionalDependencies:
'@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3
'@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3
'@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3
'@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3
'@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3
'@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3
optional: true
msgpackr@1.11.0:
optionalDependencies:
msgpackr-extract: 3.0.3
mz@2.7.0:
dependencies:
any-promise: 1.3.0
@ -7495,14 +7644,25 @@ snapshots:
negotiator@0.6.3: {}
neo-async@2.6.2: {}
node-abort-controller@3.1.1: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-gyp-build-optional-packages@5.2.2:
dependencies:
detect-libc: 2.0.3
optional: true
node-gyp-build@4.8.1: {}
node-releases@2.0.14: {}
nodemailer@6.9.14: {}
nopt@5.0.0:
dependencies:
abbrev: 1.1.1
@ -8737,6 +8897,9 @@ snapshots:
ufo@1.5.3: {}
uglify-js@3.19.1:
optional: true
ultrahtml@1.5.3: {}
undici-types@5.26.5: {}
@ -8770,6 +8933,8 @@ snapshots:
utils-merge@1.0.1: {}
uuid@9.0.1: {}
v8-compile-cache-lib@3.0.1: {}
valibot@0.31.1:
@ -8878,6 +9043,8 @@ snapshots:
word-wrap@1.2.5: {}
wordwrap@1.0.0: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0

View file

@ -1,22 +1,21 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { injectable } from 'tsyringe';
import type { HonoTypes } from '../types';
import { requireAuth } from "../middleware/auth.middleware";
import { registerEmailPasswordDto } from '$lib/dtos/register-emailpassword.dto';
import { limiter } from '../middleware/rate-limiter.middleware';
import type { Controller } from '../interfaces/controller.interface';
const app = new Hono()
.get('/me', requireAuth, async (c) => {
const user = c.var.user;
return c.json({ user });
})
.get('/user', requireAuth, async (c) => {
const user = c.var.user;
return c.json({ user });
})
.post('/login/request', zValidator('json', registerEmailPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const { email } = c.req.valid('json');
await this.loginRequestsService.create({ email });
return c.json({ message: 'Verification email sent' });
});
@injectable()
export class IamController implements Controller {
controller = new Hono<HonoTypes>();
export default app;
constructor(
) { }
routes() {
return this.controller
.get('/me', requireAuth, async (c) => {
const user = c.var.user;
return c.json({ user });
});
}
}

View file

@ -1,13 +1,26 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { registerEmailPasswordDto } from '$lib/dtos/register-emailpassword.dto';
import { inject, injectable } from 'tsyringe';
import type { HonoTypes } from '../types';
import { limiter } from '../middleware/rate-limiter.middleware';
import type { Controller } from '../interfaces/controller.interface';
import { signInEmailDto } from '$lib/dtos/signin-email.dto';
import type { LoginRequestsService } from '../services/loginrequest.service';
const app = new Hono()
.post('/', zValidator('json', registerEmailPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const { email } = c.req.valid('json');
await loginRequestsService.create({ email });
return c.json({ message: 'Verification email sent' });
});
@injectable()
export class LoginController implements Controller {
controller = new Hono<HonoTypes>();
export default app;
constructor(
@inject('LoginRequestsService') private readonly loginRequestsService: LoginRequestsService
) { }
routes() {
return this.controller
.post('/', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const { username, password } = c.req.valid('json');
await this.loginRequestsService.verify({ username, password });
return c.json({ message: 'Verification email sent' });
})
}
}

View file

@ -0,0 +1,20 @@
import { pgTable, text, uuid } from "drizzle-orm/pg-core";
import { timestamps } from '../utils';
import { usersTable } from "./users.table";
enum CredentialsType {
SECRET = 'secret',
PASSWORD = 'password',
TOTP = 'totp',
HOTP = 'hotp'
}
export const credentialsTable = pgTable('credentials', {
id: uuid('id').primaryKey().defaultRandom(),
user_id: uuid('user_id')
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
type: text('type').notNull().default(CredentialsType.PASSWORD),
secret_data: text('secret_data').notNull(),
...timestamps
});

View file

@ -1,4 +1,4 @@
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import games from './games';

View file

@ -0,0 +1,14 @@
import { pgTable, text, uuid } from "drizzle-orm/pg-core";
import { usersTable } from "./users.table";
import { timestamps } from '../utils';
export const federatedIdentityTable = pgTable('federated_identity', {
id: uuid('id').primaryKey().defaultRandom(),
user_id: uuid('user_id')
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
idenitity_provider: text('idenitity_provider').notNull(),
federated_user_id: text('federated_user_id').notNull(),
federated_username: text('federated_username').notNull(),
...timestamps
});

View file

@ -0,0 +1,8 @@
import { Hono } from 'hono';
import type { HonoTypes } from '../types';
import type { BlankSchema } from 'hono/types';
export interface Controller {
controller: Hono<HonoTypes, BlankSchema, '/'>;
routes(): any;
}

View file

@ -0,0 +1,10 @@
import { Argon2id } from "oslo/password";
export async function hash(value: string) {
const argon2 = new Argon2id()
return argon2.hash(value);
}
export function verify(hashedValue: string, value: string) {
return new Argon2id().verify(hashedValue, value);
}

View file

@ -1,3 +1,4 @@
import { container } from 'tsyringe';
import { db } from '../infrastructure/database';
// Symbol

View file

@ -1,10 +1,11 @@
// import { lucia } from '../infrastructure/auth/lucia';
import { container } from 'tsyringe';
import { lucia } from '../infrastructure/auth/lucia';
// // Symbol
// export const LuciaProvider = Symbol('LUCIA_PROVIDER');
// Symbol
export const LuciaProvider = Symbol('LUCIA_PROVIDER');
// // Type
// export type LuciaProvider = typeof lucia;
// Type
export type LuciaProvider = typeof lucia;
// // Register
// container.register<LuciaProvider>(LuciaProvider, { useValue: lucia });
// Register
container.register<LuciaProvider>(LuciaProvider, { useValue: lucia });

View file

@ -0,0 +1,35 @@
import { eq, type InferInsertModel } from "drizzle-orm";
import { credentialsTable } from "../infrastructure/database/tables/credentials.table";
import { db } from "../infrastructure/database";
import { takeFirstOrThrow } from "../infrastructure/database/utils";
export type CreateCredentials = InferInsertModel<typeof credentialsTable>;
export type UpdateCredentials = Partial<CreateCredentials>;
export class CredentialsRepository {
async findOneById(id: string) {
return db.query.credentialsTable.findFirst({
where: eq(credentialsTable.id, id)
});
}
async findOneByIdOrThrow(id: string) {
const credentials = await this.findOneById(id);
if (!credentials) throw Error('Credentials not found');
return credentials;
}
async create(data: CreateCredentials) {
return db.insert(credentialsTable).values(data).returning().then(takeFirstOrThrow);
}
async update(id: string, data: UpdateCredentials) {
return db
.update(credentialsTable)
.set(data)
.where(eq(credentialsTable.id, id))
.returning()
.then(takeFirstOrThrow);
}
}

View file

@ -35,6 +35,12 @@ export class UsersRepository {
return user;
}
async findOneByUsername(username: string) {
return db.query.usersTable.findFirst({
where: eq(usersTable.username, username)
});
}
async findOneByEmail(email: string) {
return db.query.usersTable.findFirst({
where: eq(usersTable.email, email)

View file

@ -1,3 +1,4 @@
import { injectable } from "tsyringe";
import { Argon2id } from "oslo/password";
/* ---------------------------------- Note ---------------------------------- */
@ -19,6 +20,7 @@ node_modules/.pnpm/@node-rs+argon2@1.7.0/node_modules/@node-rs/argon2/index.js:1
/* -------------------------------------------------------------------------- */
// If you don't use a hasher from oslo, which are preconfigured with recommended parameters from OWASP,
// ensure that you configure them properly.
@injectable()
export class HashingService {
private readonly hasher = new Argon2id();

View file

@ -1,4 +1,5 @@
import { lucia } from '../infrastructure/auth/lucia';
import { inject, injectable } from 'tsyringe';
import { LuciaProvider } from '../providers/lucia.provider';
/* -------------------------------------------------------------------------- */
/* Service */
@ -17,8 +18,13 @@ Create private functions to handle complex logic and keep the public methods as
simple as possible. This makes the service easier to read, test and understand.
*/
/* -------------------------------------------------------------------------- */
@injectable()
export class IamService {
constructor(
@inject(LuciaProvider) private readonly lucia: LuciaProvider,
) { }
async logout(sessionId: string) {
return lucia.invalidateSession(sessionId);
return this.lucia.invalidateSession(sessionId);
}
}

View file

@ -0,0 +1,75 @@
import { inject, injectable } from 'tsyringe';
import { BadRequest } from '../common/errors';
import { DatabaseProvider } from '../providers';
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';
@injectable()
export class LoginRequestsService {
constructor(
@inject(LuciaProvider) private readonly lucia: LuciaProvider,
@inject(DatabaseProvider) private readonly db: DatabaseProvider,
@inject(TokensService) private readonly tokensService: TokensService,
@inject(MailerService) private readonly mailerService: MailerService,
@inject(UsersRepository) private readonly usersRepository: UsersRepository,
@inject(LoginRequestsRepository) private readonly loginRequestsRepository: LoginRequestsRepository,
) { }
async validate(data: SignInEmailDto) {
}
async create(data: RegisterEmailDto) {
// generate a token, expiry date, and hash
const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm');
// save the login request to the database - ensuring we save the hashedToken
await this.loginRequestsRepository.create({ email: data.email, hashedToken, expiresAt: expiry });
// send the login request email
await this.mailerService.sendLoginRequest({
to: data.email,
props: { token: token }
});
}
async verify(data: SignInEmailDto) {
let existingUser = await this.usersRepository.findOneByUsername(data.username);
if (!existingUser) {
throw BadRequest('User not found');
}
return this.lucia.createSession(existingUser.id, {});
}
// Create a new user and send a welcome email - or other onboarding process
private async handleNewUserRegistration(email: string) {
const newUser = await this.usersRepository.create({ email, verified: true, avatar: null })
this.mailerService.sendWelcome({ to: email, props: null });
// TODO: add whatever onboarding process or extra data you need here
return newUser
}
// Fetch a valid request from the database, verify the token and burn the request if it is valid
private async fetchValidRequest(email: string, token: string) {
return await this.db.transaction(async (trx) => {
// fetch the login request
const loginRequest = await this.loginRequestsRepository.trxHost(trx).findOneByEmail(email)
if (!loginRequest) return null;
// check if the token is valid
const isValidRequest = await this.tokensService.verifyHashedToken(loginRequest.hashedToken, token);
if (!isValidRequest) return null
// if the token is valid, burn the request
await this.loginRequestsRepository.trxHost(trx).deleteById(loginRequest.id);
return loginRequest
})
}
}

View file

@ -11,14 +11,14 @@ import { injectable } from 'tsyringe';
/* -------------------------------------------------------------------------- */
/* ---------------------------------- About --------------------------------- */
/*
Services are responsible for handling business logic and data manipulation.
Services are responsible for handling business logic and data manipulation.
They genreally call on repositories or other services to complete a use-case.
*/
/* ---------------------------------- Notes --------------------------------- */
/*
Services should be kept as clean and simple as possible.
Services should be kept as clean and simple as possible.
Create private functions to handle complex logic and keep the public methods as
Create private functions to handle complex logic and keep the public methods as
simple as possible. This makes the service easier to read, test and understand.
*/
/* -------------------------------------------------------------------------- */

View file

@ -1,9 +1,11 @@
import { inject, injectable } from "tsyringe";
import { generateRandomString } from "oslo/crypto";
import { TimeSpan, createDate, type TimeSpanUnit } from 'oslo';
import { HashingService } from "./hashing.service";
@injectable()
export class TokensService {
private readonly hashingService = new HashingService();
constructor(@inject(HashingService) private readonly hashingService: HashingService) { }
generateToken() {
const alphabet = '23456789ACDEFGHJKLMNPQRSTUVWXYZ'; // alphabet with removed look-alike characters (0, 1, O, I)