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()
@injectable()
export class IamController implements Controller {
controller = new Hono<HonoTypes>();
constructor(
) { }
routes() {
return this.controller
.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' });
});
export default app;
}
}

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 });
@injectable()
export class LoginController implements Controller {
controller = new Hono<HonoTypes>();
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' });
});
export default app;
})
}
}

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

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