feat: client and server logic to manage announcements #3

Merged
naomi merged 8 commits from feat/announcements into main 2025-07-05 19:27:21 -07:00
14 changed files with 548 additions and 102 deletions
Showing only changes of commit 42bad8c6c8 - Show all commits

299
pnpm-lock.yaml generated
View File

@ -10,13 +10,13 @@ importers:
devDependencies:
'@nhcarrigan/eslint-config':
specifier: 5.2.0
version: 5.2.0(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3))(eslint@9.30.1(jiti@1.21.7))(playwright@1.53.2)(react@19.1.0)(typescript@5.8.3)(vitest@3.2.4(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1))
version: 5.2.0(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(playwright@1.53.2)(react@19.1.0)(typescript@5.8.3)(vitest@3.2.4(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3))
'@nhcarrigan/typescript-config':
specifier: 4.0.0
version: 4.0.0(typescript@5.8.3)
eslint:
specifier: 9.30.1
version: 9.30.1(jiti@1.21.7)
version: 9.30.1(jiti@2.4.2)
turbo:
specifier: 2.5.4
version: 2.5.4
@ -56,7 +56,7 @@ importers:
devDependencies:
'@angular/build':
specifier: 20.0.5
version: 20.0.5(@angular/compiler-cli@20.0.6(@angular/compiler@20.0.6)(typescript@5.8.3))(@angular/compiler@20.0.6)(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.0.6(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.0.10)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(less@4.3.0)(postcss@8.5.6)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@3.2.4(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1))
version: 20.0.5(@angular/compiler-cli@20.0.6(@angular/compiler@20.0.6)(typescript@5.8.3))(@angular/compiler@20.0.6)(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.0.6(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.0.10)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(less@4.3.0)(postcss@8.5.6)(terser@5.39.1)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.8.3)(vitest@3.2.4(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3))
'@angular/cli':
specifier: 20.0.5
version: 20.0.5(@types/node@24.0.10)(chokidar@4.0.3)
@ -93,6 +93,9 @@ importers:
'@nhcarrigan/logger':
specifier: 1.0.0
version: 1.0.0
'@prisma/client':
specifier: 6.11.1
version: 6.11.1(prisma@6.11.1(typescript@5.8.3))(typescript@5.8.3)
fastify:
specifier: 5.4.0
version: 5.4.0
@ -100,6 +103,12 @@ importers:
'@types/node':
specifier: 24.0.10
version: 24.0.10
prisma:
specifier: 6.11.1
version: 6.11.1(typescript@5.8.3)
tsx:
specifier: 4.20.3
version: 4.20.3
packages:
@ -1074,6 +1083,36 @@ packages:
resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@prisma/client@6.11.1':
resolution: {integrity: sha512-5CLFh8QP6KxRm83pJ84jaVCeSVPQr8k0L2SEtOJHwdkS57/VQDcI/wQpGmdyOZi+D9gdNabdo8tj1Uk+w+upsQ==}
engines: {node: '>=18.18'}
peerDependencies:
prisma: '*'
typescript: '>=5.1.0'
peerDependenciesMeta:
prisma:
optional: true
typescript:
optional: true
'@prisma/config@6.11.1':
resolution: {integrity: sha512-z6rCTQN741wxDq82cpdzx2uVykpnQIXalLhrWQSR0jlBVOxCIkz3HZnd8ern3uYTcWKfB3IpVAF7K2FU8t/8AQ==}
'@prisma/debug@6.11.1':
resolution: {integrity: sha512-lWRb/YSWu8l4Yum1UXfGLtqFzZkVS2ygkWYpgkbgMHn9XJlMITIgeMvJyX5GepChzhmxuSuiq/MY/kGFweOpGw==}
'@prisma/engines-version@6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9':
resolution: {integrity: sha512-swFJTOOg4tHyOM1zB/pHb3MeH0i6t7jFKn5l+ZsB23d9AQACuIRo9MouvuKGvnDogzkcjbWnXi/NvOZ0+n5Jfw==}
'@prisma/engines@6.11.1':
resolution: {integrity: sha512-6eKEcV6V8W2eZAUwX2xTktxqPM4vnx3sxz3SDtpZwjHKpC6lhOtc4vtAtFUuf5+eEqBk+dbJ9Dcaj6uQU+FNNg==}
'@prisma/fetch-engine@6.11.1':
resolution: {integrity: sha512-NBYzmkXTkj9+LxNPRSndaAeALOL1Gr3tjvgRYNqruIPlZ6/ixLeuE/5boYOewant58tnaYFZ5Ne0jFBPfGXHpQ==}
'@prisma/get-platform@6.11.1':
resolution: {integrity: sha512-b2Z8oV2gwvdCkFemBTFd0x4lsL4O2jLSx8lB7D+XqoFALOQZPa7eAPE1NU0Mj1V8gPHRxIsHnyUNtw2i92psUw==}
'@rollup/rollup-android-arm-eabi@4.40.2':
resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==}
cpu: [arm]
@ -2385,6 +2424,9 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
get-tsconfig@4.10.1:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@ -2741,8 +2783,8 @@ packages:
jasmine-core@5.8.0:
resolution: {integrity: sha512-Q9dqmpUAfptwyueW3+HqBOkSuYd9I/clZSSfN97wXE/Nr2ROFNCwIBEC1F6kb3QXS9Fcz0LjFYSDQT+BiwjuhA==}
jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
jiti@2.4.2:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
js-tokens@4.0.0:
@ -3353,6 +3395,16 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
prisma@6.11.1:
resolution: {integrity: sha512-VzJToRlV0s9Vu2bfqHiRJw73hZNCG/AyJeX+kopbu4GATTjTUdEWUteO3p4BLYoHpMS4o8pD3v6tF44BHNZI1w==}
engines: {node: '>=18.18'}
hasBin: true
peerDependencies:
typescript: '>=5.1.0'
peerDependenciesMeta:
typescript:
optional: true
proc-log@5.0.0:
resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==}
engines: {node: ^18.17.0 || >=20.5.0}
@ -3467,6 +3519,9 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
resolve@1.22.10:
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
engines: {node: '>= 0.4'}
@ -3869,6 +3924,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsx@4.20.3:
resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==}
engines: {node: '>=18.0.0'}
hasBin: true
tuf-js@3.1.0:
resolution: {integrity: sha512-3T3T04WzowbwV2FDiGXBbr81t64g1MUGGJRgT4x5o97N+8ArdhVCAF9IxFrxuSJmM3E5Asn7nKHkao0ibcZXAg==}
engines: {node: ^18.17.0 || >=20.5.0}
@ -4281,7 +4341,7 @@ snapshots:
transitivePeerDependencies:
- chokidar
'@angular/build@20.0.5(@angular/compiler-cli@20.0.6(@angular/compiler@20.0.6)(typescript@5.8.3))(@angular/compiler@20.0.6)(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.0.6(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.0.10)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(less@4.3.0)(postcss@8.5.6)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@3.2.4(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1))':
'@angular/build@20.0.5(@angular/compiler-cli@20.0.6(@angular/compiler@20.0.6)(typescript@5.8.3))(@angular/compiler@20.0.6)(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.0.6(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.0.10)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(less@4.3.0)(postcss@8.5.6)(terser@5.39.1)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.8.3)(vitest@3.2.4(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3))':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2000.5(chokidar@4.0.3)
@ -4291,7 +4351,7 @@ snapshots:
'@babel/helper-annotate-as-pure': 7.27.1
'@babel/helper-split-export-declaration': 7.24.7
'@inquirer/confirm': 5.1.10(@types/node@24.0.10)
'@vitejs/plugin-basic-ssl': 2.0.0(vite@6.3.5(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1))
'@vitejs/plugin-basic-ssl': 2.0.0(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3))
beasties: 0.3.4
browserslist: 4.25.1
esbuild: 0.25.5
@ -4311,7 +4371,7 @@ snapshots:
tinyglobby: 0.2.13
tslib: 2.8.1
typescript: 5.8.3
vite: 6.3.5(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)
vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3)
watchpack: 2.4.2
optionalDependencies:
'@angular/core': 20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)(zone.js@0.15.1)
@ -4320,7 +4380,7 @@ snapshots:
less: 4.3.0
lmdb: 3.3.0
postcss: 8.5.6
vitest: 3.2.4(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)
vitest: 3.2.4(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3)
transitivePeerDependencies:
- '@types/node'
- chokidar
@ -4662,22 +4722,22 @@ snapshots:
'@esbuild/win32-x64@0.25.5':
optional: true
'@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.30.1(jiti@1.21.7))':
'@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.30.1(jiti@2.4.2))':
dependencies:
escape-string-regexp: 4.0.0
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
ignore: 5.3.2
'@eslint-community/eslint-utils@4.7.0(eslint@9.30.1(jiti@1.21.7))':
'@eslint-community/eslint-utils@4.7.0(eslint@9.30.1(jiti@2.4.2))':
dependencies:
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
'@eslint/compat@1.2.4(eslint@9.30.1(jiti@1.21.7))':
'@eslint/compat@1.2.4(eslint@9.30.1(jiti@2.4.2))':
optionalDependencies:
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
'@eslint/config-array@0.21.0':
dependencies:
@ -5046,29 +5106,29 @@ snapshots:
'@napi-rs/nice-win32-x64-msvc': 1.0.2
optional: true
'@nhcarrigan/eslint-config@5.2.0(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3))(eslint@9.30.1(jiti@1.21.7))(playwright@1.53.2)(react@19.1.0)(typescript@5.8.3)(vitest@3.2.4(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1))':
'@nhcarrigan/eslint-config@5.2.0(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(playwright@1.53.2)(react@19.1.0)(typescript@5.8.3)(vitest@3.2.4(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3))':
dependencies:
'@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.30.1(jiti@1.21.7))
'@eslint/compat': 1.2.4(eslint@9.30.1(jiti@1.21.7))
'@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.30.1(jiti@2.4.2))
'@eslint/compat': 1.2.4(eslint@9.30.1(jiti@2.4.2))
'@eslint/eslintrc': 3.2.0
'@eslint/js': 9.17.0
'@stylistic/eslint-plugin': 2.12.1(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
'@typescript-eslint/eslint-plugin': 8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3))(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
'@typescript-eslint/parser': 8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
'@vitest/eslint-plugin': 1.1.24(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3))(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)(vitest@3.2.4(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1))
eslint: 9.30.1(jiti@1.21.7)
eslint-plugin-deprecation: 3.0.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3))(eslint@9.30.1(jiti@1.21.7))
eslint-plugin-jsdoc: 50.6.1(eslint@9.30.1(jiti@1.21.7))
eslint-plugin-playwright: 2.1.0(eslint@9.30.1(jiti@1.21.7))
eslint-plugin-react: 7.37.3(eslint@9.30.1(jiti@1.21.7))
'@stylistic/eslint-plugin': 2.12.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/eslint-plugin': 8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/parser': 8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
'@vitest/eslint-plugin': 1.1.24(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3))
eslint: 9.30.1(jiti@2.4.2)
eslint-plugin-deprecation: 3.0.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))
eslint-plugin-jsdoc: 50.6.1(eslint@9.30.1(jiti@2.4.2))
eslint-plugin-playwright: 2.1.0(eslint@9.30.1(jiti@2.4.2))
eslint-plugin-react: 7.37.3(eslint@9.30.1(jiti@2.4.2))
eslint-plugin-sort-keys-fix: 1.1.2
eslint-plugin-unicorn: 56.0.1(eslint@9.30.1(jiti@1.21.7))
eslint-plugin-unicorn: 56.0.1(eslint@9.30.1(jiti@2.4.2))
globals: 15.14.0
playwright: 1.53.2
react: 19.1.0
typescript: 5.8.3
vitest: 3.2.4(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)
vitest: 3.2.4(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3)
transitivePeerDependencies:
- '@typescript-eslint/utils'
- eslint-import-resolver-typescript
@ -5218,6 +5278,36 @@ snapshots:
'@pkgr/core@0.1.2': {}
'@prisma/client@6.11.1(prisma@6.11.1(typescript@5.8.3))(typescript@5.8.3)':
optionalDependencies:
prisma: 6.11.1(typescript@5.8.3)
typescript: 5.8.3
'@prisma/config@6.11.1':
dependencies:
jiti: 2.4.2
'@prisma/debug@6.11.1': {}
'@prisma/engines-version@6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9': {}
'@prisma/engines@6.11.1':
dependencies:
'@prisma/debug': 6.11.1
'@prisma/engines-version': 6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9
'@prisma/fetch-engine': 6.11.1
'@prisma/get-platform': 6.11.1
'@prisma/fetch-engine@6.11.1':
dependencies:
'@prisma/debug': 6.11.1
'@prisma/engines-version': 6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9
'@prisma/get-platform': 6.11.1
'@prisma/get-platform@6.11.1':
dependencies:
'@prisma/debug': 6.11.1
'@rollup/rollup-android-arm-eabi@4.40.2':
optional: true
@ -5382,10 +5472,10 @@ snapshots:
'@socket.io/component-emitter@3.1.2': {}
'@stylistic/eslint-plugin@2.12.1(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)':
'@stylistic/eslint-plugin@2.12.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
eslint: 9.30.1(jiti@1.21.7)
'@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.30.1(jiti@2.4.2)
eslint-visitor-keys: 4.2.1
espree: 10.4.0
estraverse: 5.3.0
@ -5427,15 +5517,15 @@ snapshots:
'@types/normalize-package-data@2.4.4': {}
'@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3))(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)':
'@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
'@typescript-eslint/parser': 8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/scope-manager': 8.19.0
'@typescript-eslint/type-utils': 8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
'@typescript-eslint/utils': 8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
'@typescript-eslint/type-utils': 8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/utils': 8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.19.0
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
graphemer: 1.4.0
ignore: 5.3.2
natural-compare: 1.4.0
@ -5444,14 +5534,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)':
'@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.19.0
'@typescript-eslint/types': 8.19.0
'@typescript-eslint/typescript-estree': 8.19.0(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.19.0
debug: 4.4.1
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
@ -5484,12 +5574,12 @@ snapshots:
dependencies:
typescript: 5.8.3
'@typescript-eslint/type-utils@8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)':
'@typescript-eslint/type-utils@8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@typescript-eslint/typescript-estree': 8.19.0(typescript@5.8.3)
'@typescript-eslint/utils': 8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
'@typescript-eslint/utils': 8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
debug: 4.4.1
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
ts-api-utils: 1.4.3(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
@ -5546,35 +5636,35 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@7.18.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)':
'@typescript-eslint/utils@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@1.21.7))
'@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2))
'@typescript-eslint/scope-manager': 7.18.0
'@typescript-eslint/types': 7.18.0
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3)
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
transitivePeerDependencies:
- supports-color
- typescript
'@typescript-eslint/utils@8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)':
'@typescript-eslint/utils@8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@1.21.7))
'@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2))
'@typescript-eslint/scope-manager': 8.19.0
'@typescript-eslint/types': 8.19.0
'@typescript-eslint/typescript-estree': 8.19.0(typescript@5.8.3)
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)':
'@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@1.21.7))
'@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2))
'@typescript-eslint/scope-manager': 8.35.1
'@typescript-eslint/types': 8.35.1
'@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3)
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
@ -5594,17 +5684,17 @@ snapshots:
'@typescript-eslint/types': 8.35.1
eslint-visitor-keys: 4.2.1
'@vitejs/plugin-basic-ssl@2.0.0(vite@6.3.5(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1))':
'@vitejs/plugin-basic-ssl@2.0.0(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3))':
dependencies:
vite: 6.3.5(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)
vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3)
'@vitest/eslint-plugin@1.1.24(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3))(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)(vitest@3.2.4(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1))':
'@vitest/eslint-plugin@1.1.24(@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)(vitest@3.2.4(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3))':
dependencies:
'@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
eslint: 9.30.1(jiti@1.21.7)
'@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.30.1(jiti@2.4.2)
optionalDependencies:
typescript: 5.8.3
vitest: 3.2.4(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)
vitest: 3.2.4(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3)
'@vitest/expect@3.2.4':
dependencies:
@ -5614,13 +5704,13 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@7.0.1(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1))':
'@vitest/mocker@3.2.4(vite@7.0.1(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
vite: 7.0.1(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)
vite: 7.0.1(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3)
'@vitest/pretty-format@3.2.4':
dependencies:
@ -6359,27 +6449,27 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.30.1(jiti@1.21.7)):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.30.1(jiti@2.4.2)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
eslint: 9.30.1(jiti@1.21.7)
'@typescript-eslint/parser': 8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.30.1(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
transitivePeerDependencies:
- supports-color
eslint-plugin-deprecation@3.0.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3):
eslint-plugin-deprecation@3.0.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3):
dependencies:
'@typescript-eslint/utils': 7.18.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
eslint: 9.30.1(jiti@1.21.7)
'@typescript-eslint/utils': 7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.30.1(jiti@2.4.2)
ts-api-utils: 1.4.3(typescript@5.8.3)
tslib: 2.8.1
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3))(eslint@9.30.1(jiti@1.21.7)):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@ -6388,9 +6478,9 @@ snapshots:
array.prototype.flatmap: 1.3.3
debug: 3.2.7
doctrine: 2.1.0
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.30.1(jiti@1.21.7))
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.30.1(jiti@2.4.2))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -6402,20 +6492,20 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
'@typescript-eslint/parser': 8.19.0(eslint@9.30.1(jiti@1.21.7))(typescript@5.8.3)
'@typescript-eslint/parser': 8.19.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-jsdoc@50.6.1(eslint@9.30.1(jiti@1.21.7)):
eslint-plugin-jsdoc@50.6.1(eslint@9.30.1(jiti@2.4.2)):
dependencies:
'@es-joy/jsdoccomment': 0.49.0
are-docs-informative: 0.0.2
comment-parser: 1.4.1
debug: 4.4.1
escape-string-regexp: 4.0.0
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
espree: 10.4.0
esquery: 1.6.0
parse-imports: 2.2.1
@ -6425,12 +6515,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-playwright@2.1.0(eslint@9.30.1(jiti@1.21.7)):
eslint-plugin-playwright@2.1.0(eslint@9.30.1(jiti@2.4.2)):
dependencies:
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
globals: 13.24.0
eslint-plugin-react@7.37.3(eslint@9.30.1(jiti@1.21.7)):
eslint-plugin-react@7.37.3(eslint@9.30.1(jiti@2.4.2)):
dependencies:
array-includes: 3.1.9
array.prototype.findlast: 1.2.5
@ -6438,7 +6528,7 @@ snapshots:
array.prototype.tosorted: 1.1.4
doctrine: 2.1.0
es-iterator-helpers: 1.2.1
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
estraverse: 5.3.0
hasown: 2.0.2
jsx-ast-utils: 3.3.5
@ -6459,14 +6549,14 @@ snapshots:
natural-compare: 1.4.0
requireindex: 1.2.0
eslint-plugin-unicorn@56.0.1(eslint@9.30.1(jiti@1.21.7)):
eslint-plugin-unicorn@56.0.1(eslint@9.30.1(jiti@2.4.2)):
dependencies:
'@babel/helper-validator-identifier': 7.27.1
'@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@1.21.7))
'@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2))
ci-info: 4.2.0
clean-regexp: 1.0.0
core-js-compat: 3.43.0
eslint: 9.30.1(jiti@1.21.7)
eslint: 9.30.1(jiti@2.4.2)
esquery: 1.6.0
globals: 15.14.0
indent-string: 4.0.0
@ -6490,9 +6580,9 @@ snapshots:
eslint-visitor-keys@4.2.1: {}
eslint@9.30.1(jiti@1.21.7):
eslint@9.30.1(jiti@2.4.2):
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@1.21.7))
'@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2))
'@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.21.0
'@eslint/config-helpers': 0.3.0
@ -6528,7 +6618,7 @@ snapshots:
natural-compare: 1.4.0
optionator: 0.9.4
optionalDependencies:
jiti: 1.21.7
jiti: 2.4.2
transitivePeerDependencies:
- supports-color
@ -6754,6 +6844,10 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.3.0
get-tsconfig@4.10.1:
dependencies:
resolve-pkg-maps: 1.0.0
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@ -7132,8 +7226,7 @@ snapshots:
jasmine-core@5.8.0: {}
jiti@1.21.7:
optional: true
jiti@2.4.2: {}
js-tokens@4.0.0: {}
@ -7838,6 +7931,13 @@ snapshots:
prelude-ls@1.2.1: {}
prisma@6.11.1(typescript@5.8.3):
dependencies:
'@prisma/config': 6.11.1
'@prisma/engines': 6.11.1
optionalDependencies:
typescript: 5.8.3
proc-log@5.0.0: {}
process-warning@4.0.1: {}
@ -7944,6 +8044,8 @@ snapshots:
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
resolve@1.22.10:
dependencies:
is-core-module: 2.16.1
@ -8452,6 +8554,13 @@ snapshots:
tslib@2.8.1: {}
tsx@4.20.3:
dependencies:
esbuild: 0.25.5
get-tsconfig: 4.10.1
optionalDependencies:
fsevents: 2.3.3
tuf-js@3.1.0:
dependencies:
'@tufjs/models': 3.0.1
@ -8583,13 +8692,13 @@ snapshots:
vary@1.1.2: {}
vite-node@3.2.4(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1):
vite-node@3.2.4(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3):
dependencies:
cac: 6.7.14
debug: 4.4.1
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 7.0.1(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)
vite: 7.0.1(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3)
transitivePeerDependencies:
- '@types/node'
- jiti
@ -8604,7 +8713,7 @@ snapshots:
- tsx
- yaml
vite@6.3.5(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1):
vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3):
dependencies:
esbuild: 0.25.5
fdir: 6.4.6(picomatch@4.0.2)
@ -8615,12 +8724,13 @@ snapshots:
optionalDependencies:
'@types/node': 24.0.10
fsevents: 2.3.3
jiti: 1.21.7
jiti: 2.4.2
less: 4.3.0
sass: 1.88.0
terser: 5.39.1
tsx: 4.20.3
vite@7.0.1(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1):
vite@7.0.1(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3):
dependencies:
esbuild: 0.25.5
fdir: 6.4.6(picomatch@4.0.2)
@ -8631,16 +8741,17 @@ snapshots:
optionalDependencies:
'@types/node': 24.0.10
fsevents: 2.3.3
jiti: 1.21.7
jiti: 2.4.2
less: 4.3.0
sass: 1.88.0
terser: 5.39.1
tsx: 4.20.3
vitest@3.2.4(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1):
vitest@3.2.4(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@7.0.1(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1))
'@vitest/mocker': 3.2.4(vite@7.0.1(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@ -8658,8 +8769,8 @@ snapshots:
tinyglobby: 0.2.14
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 7.0.1(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)
vite-node: 3.2.4(@types/node@24.0.10)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)
vite: 7.0.1(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3)
vite-node: 3.2.4(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(tsx@4.20.3)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.0.10

5
server/dev.env Normal file
View File

@ -0,0 +1,5 @@
LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth"
MONGO_URI="op://Environment Variables - Naomi/Hikari/mongo_uri"
DISCORD_TOKEN="op://Environment Variables - Naomi/Hikari/discord_token"
FORUM_API_KEY="op://Environment Variables - Naomi/Hikari/discourse_key"
ANNOUNCEMENT_TOKEN="op://Environment Variables - Naomi/Hikari/announcement_token"

View File

@ -1,5 +1,18 @@
import NaomisConfig from '@nhcarrigan/eslint-config';
export default [
...NaomisConfig
...NaomisConfig,
{
files: ["src/routes/*.ts"],
rules: {
"max-lines-per-function": "off",
}
},
{
files: ["src/routes/*.ts"],
rules: {
// We turn this off so we can use the async plugin syntax without needing to await.
"@typescript-eslint/require-await": "off",
}
}
]

View File

@ -6,6 +6,7 @@
"type": "module",
"scripts": {
"lint": "eslint ./src --max-warnings 0",
"dev": "op run --env-file=./dev.env -- tsx watch ./src/index.ts",
"build": "tsc",
"start": "op run --env-file=./prod.env -- node ./prod/index.js",
"test": "echo 'No tests yet' && exit 0"
@ -16,9 +17,12 @@
"packageManager": "pnpm@10.12.3",
"dependencies": {
"@nhcarrigan/logger": "1.0.0",
"@prisma/client": "6.11.1",
"fastify": "5.4.0"
},
"devDependencies": {
"@types/node": "24.0.10"
"@types/node": "24.0.10",
"prisma": "6.11.1",
"tsx": "4.20.3"
}
}

View File

@ -0,0 +1,19 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("MONGO_URI")
}
model Announcements {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String
type String
createdAt DateTime @default(now()) @unique
}

View File

@ -1 +1,5 @@
LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth"
MONGO_URI="op://Environment Variables - Naomi/Hikari/mongo_uri"
DISCORD_TOKEN="op://Environment Variables - Naomi/Hikari/discord_token"
FORUM_API_KEY="op://Environment Variables - Naomi/Hikari/discourse_key"
ANNOUNCEMENT_TOKEN="op://Environment Variables - Naomi/Hikari/announcement_token"

View File

@ -0,0 +1,15 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/**
* If you want a route to allow any origin for CORS, add
* the full path to this array.
*/
export const routesWithoutCors = [
"/",
"/announcement",
"/health",
];

24
server/src/db/database.ts Normal file
View File

@ -0,0 +1,24 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { PrismaClient } from "@prisma/client";
class Database {
private readonly instance: PrismaClient;
public constructor() {
this.instance = new PrismaClient();
void this.instance.$connect();
}
public getInstance(): PrismaClient {
return this.instance;
}
}
const database = new Database();
export { database };

28
server/src/hooks/cors.ts Normal file
View File

@ -0,0 +1,28 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { onRequestHookHandler } from "fastify";
/**
* Ensures that form submissions only come from our web application.
* @param request - The request payload from the server.
* @param response - The reply handler from Fastify.
* @returns A Fastify reply if the request is invalid, otherwise undefined.
*/
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- For reasons I cannot comprehend, Fastify seems to require us to return a request?
export const corsHook: onRequestHookHandler = async(request, response) => {
if (!request.url.startsWith("/submit")) {
return undefined;
}
if (request.headers.origin !== "https://forms.nhcarrigan.com") {
return await response.
status(403).
send({
error: "Forms can only be submitted through our website. Thanks.",
});
}
return undefined;
};

View File

@ -5,19 +5,19 @@
*/
import fastify from "fastify";
import { corsHook } from "./hooks/cors.js";
import { announcementRoutes } from "./routes/announcement.js";
import { baseRoutes } from "./routes/base.js";
import { logger } from "./utils/logger.js";
const server = fastify({
logger: false,
});
server.get("/", async(_request, reply) => {
reply.redirect("https://hikari.nhcarrigan.com");
});
server.addHook("preHandler", corsHook);
server.get("/health", async(_request, reply) => {
reply.status(200).send("OK~!");
});
server.register(baseRoutes);
server.register(announcementRoutes);
server.listen({ port: 20_000 }, (error) => {
if (error) {

View File

@ -0,0 +1,65 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable @typescript-eslint/naming-convention -- we are making raw API calls. */
const channelIds = {
community: "1386105484313886820",
products: "1386105452881776661",
} as const;
const roleIds = {
community: "1386107941224054895",
products: "1386107909699666121",
} as const;
/**
* Forwards an announcement to our Discord server.
* @param title - The title of the announcement.
* @param content - The main body of the announcement.
* @param type - Whether the announcement is for a product or community.
* @returns A message indicating the success or failure of the operation.
*/
export const announceOnDiscord = async(
title: string,
content: string,
type: "products" | "community",
): Promise<string> => {
const messageRequest = await fetch(
`https://discord.com/api/v10/channels/${channelIds[type]}/messages`,
{
body: JSON.stringify({
allowed_mentions: { parse: [ "users", "roles" ] },
content: `# ${title}\n\n${content}\n-# <@&${roleIds[type]}>`,
}),
headers: {
"Authorization": `Bot ${process.env.DISCORD_TOKEN ?? ""}`,
"Content-Type": "application/json",
},
method: "POST",
},
);
if (messageRequest.status !== 200) {
return "Failed to send message to Discord.";
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- fetch does not accept generics.
const message = await messageRequest.json() as { id?: string };
if (message.id === undefined) {
return "Failed to parse message ID, cannot crosspost.";
}
const crosspostRequest = await fetch(
`https://discord.com/api/v10/channels/${channelIds[type]}/messages/${message.id}/crosspost`,
{
headers: {
"Authorization": `Bot ${process.env.DISCORD_TOKEN ?? ""}`,
"Content-Type": "application/json",
},
method: "POST",
},
);
if (!crosspostRequest.ok) {
return "Failed to crosspost message to Discord.";
}
return "Successfully sent and published message to Discord.";
};

View File

@ -0,0 +1,40 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable @typescript-eslint/naming-convention -- we are making raw API calls. */
/**
* Forwards an announcement to our Discord server.
* @param title - The title of the announcement.
* @param content - The main body of the announcement.
* @param type - Whether the announcement is for a product or community.
* @returns A message indicating the success or failure of the operation.
*/
export const announceOnForum = async(
title: string,
content: string,
type: "products" | "community",
): Promise<string> => {
const forumRequest = await fetch(
`https://forum.nhcarrigan.com/posts.json`,
{
body: JSON.stringify({
category: 14,
raw: content,
tags: [ type ],
title: title,
}),
headers: {
"Api-Key": process.env.FORUM_API_KEY ?? "",
"Api-Username": "Hikari",
"Content-Type": "application/json",
},
method: "POST",
},
);
if (forumRequest.status !== 200) {
return "Failed to send message to forum.";
}
return "Successfully sent message to forum.";
};

View File

@ -0,0 +1,95 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { database } from "../db/database.js";
import { announceOnDiscord } from "../modules/announceOnDiscord.js";
import { announceOnForum } from "../modules/announceOnForum.js";
import type { FastifyPluginAsync } from "fastify";
/**
* Mounts the entry routes for the application. These routes
* should not require CORS, as they are used by external services
* such as our uptime monitor.
* @param server - The Fastify server instance.
*/
export const announcementRoutes: FastifyPluginAsync = async(server) => {
server.get("/announcements", async(_request, reply) => {
const announcements = await database.getInstance().announcements.findMany({
orderBy: {
createdAt: "desc",
},
take: 10,
});
return await reply.status(200).type("application/json").
send(announcements);
});
// eslint-disable-next-line @typescript-eslint/naming-convention -- Fastify requires Body instead of body.
server.post<{ Body: { title: string; content: string; type: string } }>(
"/announcement",
// eslint-disable-next-line complexity -- This is a complex route, but it is necessary to validate the announcement.
async(request, reply) => {
const token = request.headers.authorization;
if (token === undefined || token !== process.env.ANNOUNCEMENT_TOKEN) {
return await reply.status(401).send({
error:
// eslint-disable-next-line stylistic/max-len -- Big boi string.
"This endpoint requires a special auth token. If you believe you should have access, please contact Naomi.",
});
}
const { title, content, type } = request.body;
if (
typeof title !== "string"
|| typeof content !== "string"
|| typeof type !== "string"
|| title.length === 0
|| content.length === 0
|| type.length === 0
) {
return await reply.status(400).send({
error: "Missing required fields.",
});
}
if (title.length < 20) {
return await reply.status(400).send({
error:
// eslint-disable-next-line stylistic/max-len -- Big boi string.
"Title must be at least 20 characters long so that it may be posted on our forum.",
});
}
if (content.length < 50) {
return await reply.status(400).send({
error:
// eslint-disable-next-line stylistic/max-len -- Big boi string.
"Content must be at least 50 characters long so that it may be posted on our forum.",
});
}
if (type !== "products" && type !== "community") {
return await reply.status(400).send({
error: "Invalid announcement type.",
});
}
await database.getInstance().announcements.create({
data: {
content,
title,
type,
},
});
const discord = await announceOnDiscord(title, content, type);
const forum = await announceOnForum(title, content, type);
return await reply.status(201).send({
message: `Announcement processed. Discord: ${discord}, Forum: ${forum}`,
});
},
);
};

23
server/src/routes/base.ts Normal file
View File

@ -0,0 +1,23 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { FastifyPluginAsync } from "fastify";
/**
* Mounts the entry routes for the application. These routes
* should not require CORS, as they are used by external services
* such as our uptime monitor.
* @param server - The Fastify server instance.
*/
export const baseRoutes: FastifyPluginAsync = async(server) => {
server.get("/", async(_request, reply) => {
return await reply.redirect("https://hikari.nhcarrigan.com");
});
server.get("/health", async(_request, reply) => {
return await reply.status(200).send("OK~!");
});
};