add login with sessian and cleanup
All checks were successful
Build and Push Docker Images / build (push) Successful in 2m34s
All checks were successful
Build and Push Docker Images / build (push) Successful in 2m34s
This commit is contained in:
27
server/.dockerignore
Normal file
27
server/.dockerignore
Normal file
@@ -0,0 +1,27 @@
|
||||
# Node modules
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Local dev / editor
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# Build output
|
||||
dist
|
||||
build
|
||||
.vite
|
||||
|
||||
# TypeScript build info
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment files (if you want them injected via Docker ENV)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
@@ -8,9 +8,17 @@
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^11.1.0",
|
||||
"@fastify/static": "^8.2.0",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.1.0",
|
||||
"fastify": "^5.6.1",
|
||||
"helmet": "^8.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"ldapts": "^8.0.9",
|
||||
"mongodb": "^6.20.0",
|
||||
"mongoose": "^8.19.1",
|
||||
"node": "^24.10.0",
|
||||
|
||||
223
server/pnpm-lock.yaml
generated
223
server/pnpm-lock.yaml
generated
@@ -14,15 +14,39 @@ importers:
|
||||
'@fastify/static':
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0
|
||||
'@types/cookie-parser':
|
||||
specifier: ^1.4.9
|
||||
version: 1.4.9(@types/express@5.0.3)
|
||||
'@types/jsonwebtoken':
|
||||
specifier: ^9.0.10
|
||||
version: 9.0.10
|
||||
cookie-parser:
|
||||
specifier: ^1.4.7
|
||||
version: 1.4.7
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.5
|
||||
dotenv:
|
||||
specifier: ^17.2.3
|
||||
version: 17.2.3
|
||||
express:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
express-rate-limit:
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.0(express@5.1.0)
|
||||
fastify:
|
||||
specifier: ^5.6.1
|
||||
version: 5.6.1
|
||||
helmet:
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.0
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.2
|
||||
ldapts:
|
||||
specifier: ^8.0.9
|
||||
version: 8.0.9
|
||||
mongodb:
|
||||
specifier: ^6.20.0
|
||||
version: 6.20.0
|
||||
@@ -192,12 +216,20 @@ packages:
|
||||
'@tsconfig/node16@1.0.4':
|
||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||
|
||||
'@types/asn1@0.2.4':
|
||||
resolution: {integrity: sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==}
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
'@types/cookie-parser@1.4.9':
|
||||
resolution: {integrity: sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==}
|
||||
peerDependencies:
|
||||
'@types/express': '*'
|
||||
|
||||
'@types/cors@2.8.19':
|
||||
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
||||
|
||||
@@ -216,9 +248,15 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||
|
||||
'@types/mime@1.3.5':
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
||||
'@types/ms@2.1.0':
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
|
||||
'@types/node@24.7.2':
|
||||
resolution: {integrity: sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==}
|
||||
|
||||
@@ -304,6 +342,9 @@ packages:
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
asn1@0.2.6:
|
||||
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
|
||||
|
||||
atomic-sleep@1.0.0:
|
||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@@ -333,6 +374,9 @@ packages:
|
||||
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
|
||||
engines: {node: '>=16.20.1'}
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
bytes@3.1.2:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -379,6 +423,13 @@ packages:
|
||||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
cookie-parser@1.4.7:
|
||||
resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
cookie-signature@1.0.6:
|
||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||
|
||||
cookie-signature@1.2.2:
|
||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||
engines: {node: '>=6.6.0'}
|
||||
@@ -406,6 +457,15 @@ packages:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
debug@4.4.1:
|
||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -430,6 +490,10 @@ packages:
|
||||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
|
||||
dotenv@17.2.3:
|
||||
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -437,6 +501,9 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
@@ -515,6 +582,12 @@ packages:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
express-rate-limit@8.1.0:
|
||||
resolution: {integrity: sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA==}
|
||||
engines: {node: '>= 16'}
|
||||
peerDependencies:
|
||||
express: '>= 4.11'
|
||||
|
||||
express@5.1.0:
|
||||
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
|
||||
engines: {node: '>= 18'}
|
||||
@@ -649,6 +722,10 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
helmet@8.1.0:
|
||||
resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
http-errors@2.0.0:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -679,6 +756,10 @@ packages:
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ip-address@10.0.1:
|
||||
resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -736,6 +817,16 @@ packages:
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
jsonwebtoken@9.0.2:
|
||||
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
|
||||
jwa@1.4.2:
|
||||
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
|
||||
|
||||
jws@3.2.2:
|
||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||
|
||||
kareem@2.6.3:
|
||||
resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -743,6 +834,10 @@ packages:
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
ldapts@8.0.9:
|
||||
resolution: {integrity: sha512-6UwfVFUX0Yp5XFY8ST0p9sytpmHGNm32GehI/dq4HuA3pL5kh0AceHBSfowv+cutIJFQnfBZmBo/6cnj87JDqA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -754,9 +849,30 @@ packages:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
lodash.includes@4.3.0:
|
||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||
|
||||
lodash.isboolean@3.0.3:
|
||||
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||
|
||||
lodash.isinteger@4.0.4:
|
||||
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||
|
||||
lodash.isnumber@3.0.3:
|
||||
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||
|
||||
lodash.isplainobject@4.0.6:
|
||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||
|
||||
lodash.isstring@4.0.1:
|
||||
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
lodash.once@4.1.1:
|
||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||
|
||||
lru-cache@11.2.2:
|
||||
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -1111,6 +1227,9 @@ packages:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
strict-event-emitter-types@2.0.0:
|
||||
resolution: {integrity: sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1202,6 +1321,10 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
uuid@11.1.0:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
|
||||
v8-compile-cache-lib@3.0.1:
|
||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||
|
||||
@@ -1397,6 +1520,10 @@ snapshots:
|
||||
|
||||
'@tsconfig/node16@1.0.4': {}
|
||||
|
||||
'@types/asn1@0.2.4':
|
||||
dependencies:
|
||||
'@types/node': 24.7.2
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
@@ -1406,6 +1533,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 24.7.2
|
||||
|
||||
'@types/cookie-parser@1.4.9(@types/express@5.0.3)':
|
||||
dependencies:
|
||||
'@types/express': 5.0.3
|
||||
|
||||
'@types/cors@2.8.19':
|
||||
dependencies:
|
||||
'@types/node': 24.7.2
|
||||
@@ -1429,8 +1560,15 @@ snapshots:
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
'@types/node': 24.7.2
|
||||
|
||||
'@types/mime@1.3.5': {}
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
'@types/node@24.7.2':
|
||||
dependencies:
|
||||
undici-types: 7.14.0
|
||||
@@ -1514,6 +1652,10 @@ snapshots:
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
asn1@0.2.6:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
|
||||
avvio@9.1.0:
|
||||
@@ -1550,6 +1692,8 @@ snapshots:
|
||||
|
||||
bson@6.10.4: {}
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
bytes@3.1.2: {}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
@@ -1599,6 +1743,13 @@ snapshots:
|
||||
|
||||
content-type@1.0.5: {}
|
||||
|
||||
cookie-parser@1.4.7:
|
||||
dependencies:
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.0.6
|
||||
|
||||
cookie-signature@1.0.6: {}
|
||||
|
||||
cookie-signature@1.2.2: {}
|
||||
|
||||
cookie@0.7.2: {}
|
||||
@@ -1620,6 +1771,10 @@ snapshots:
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.3(supports-color@5.5.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -1634,6 +1789,8 @@ snapshots:
|
||||
|
||||
diff@4.0.2: {}
|
||||
|
||||
dotenv@17.2.3: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -1642,6 +1799,10 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
@@ -1731,6 +1892,11 @@ snapshots:
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
express-rate-limit@8.1.0(express@5.1.0):
|
||||
dependencies:
|
||||
express: 5.1.0
|
||||
ip-address: 10.0.1
|
||||
|
||||
express@5.1.0:
|
||||
dependencies:
|
||||
accepts: 2.0.0
|
||||
@@ -1919,6 +2085,8 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
helmet@8.1.0: {}
|
||||
|
||||
http-errors@2.0.0:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
@@ -1948,6 +2116,8 @@ snapshots:
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ip-address@10.0.1: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
ipaddr.js@2.2.0: {}
|
||||
@@ -1990,12 +2160,47 @@ snapshots:
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
jsonwebtoken@9.0.2:
|
||||
dependencies:
|
||||
jws: 3.2.2
|
||||
lodash.includes: 4.3.0
|
||||
lodash.isboolean: 3.0.3
|
||||
lodash.isinteger: 4.0.4
|
||||
lodash.isnumber: 3.0.3
|
||||
lodash.isplainobject: 4.0.6
|
||||
lodash.isstring: 4.0.1
|
||||
lodash.once: 4.1.1
|
||||
ms: 2.1.3
|
||||
semver: 7.7.3
|
||||
|
||||
jwa@1.4.2:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@3.2.2:
|
||||
dependencies:
|
||||
jwa: 1.4.2
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
kareem@2.6.3: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
ldapts@8.0.9:
|
||||
dependencies:
|
||||
'@types/asn1': 0.2.4
|
||||
asn1: 0.2.6
|
||||
debug: 4.4.1
|
||||
strict-event-emitter-types: 2.0.0
|
||||
uuid: 11.1.0
|
||||
whatwg-url: 14.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
levn@0.4.1:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -2011,8 +2216,22 @@ snapshots:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
||||
lodash.includes@4.3.0: {}
|
||||
|
||||
lodash.isboolean@3.0.3: {}
|
||||
|
||||
lodash.isinteger@4.0.4: {}
|
||||
|
||||
lodash.isnumber@3.0.3: {}
|
||||
|
||||
lodash.isplainobject@4.0.6: {}
|
||||
|
||||
lodash.isstring@4.0.1: {}
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lodash.once@4.1.1: {}
|
||||
|
||||
lru-cache@11.2.2: {}
|
||||
|
||||
make-error@1.3.6: {}
|
||||
@@ -2345,6 +2564,8 @@ snapshots:
|
||||
|
||||
statuses@2.0.1: {}
|
||||
|
||||
strict-event-emitter-types@2.0.0: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
@@ -2433,6 +2654,8 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
||||
v8-compile-cache-lib@3.0.1: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
117
server/src/api/v1/auth/index.ts
Normal file
117
server/src/api/v1/auth/index.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import express, { type Router, type Request, type Response } from 'express'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
import { UserModel } from '../../../models/user.model.ts'
|
||||
|
||||
import ldapAuth from './ldap.ts'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!
|
||||
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET!
|
||||
|
||||
function createAccessToken(user: any) {
|
||||
return jwt.sign(
|
||||
{ sub: user._id, role: user.role },
|
||||
ACCESS_TOKEN_SECRET,
|
||||
{ expiresIn: '15m' },
|
||||
)
|
||||
}
|
||||
|
||||
function createRefreshToken(user: any) {
|
||||
return jwt.sign(
|
||||
{ sub: user._id },
|
||||
REFRESH_TOKEN_SECRET,
|
||||
{ expiresIn: '7d' },
|
||||
)
|
||||
}
|
||||
|
||||
router.post('/login', async (req: Request, res: Response) => {
|
||||
const { email, username, password } = req.body
|
||||
if (!username || !password) return res.status(400).json({ error: 'Missing credentials' })
|
||||
|
||||
try {
|
||||
const ldapUser = await ldapAuth({ username, password })
|
||||
|
||||
if (!ldapUser.auth) return res.status(401).json({ error: 'Invalid credentials' })
|
||||
|
||||
let user = await UserModel.findOne({ username: ldapUser.user.cn })
|
||||
if (!user) {
|
||||
user = await UserModel.create({
|
||||
username: ldapUser.user.cn,
|
||||
email: ldapUser.user.dn,
|
||||
refresh_token: '',
|
||||
})
|
||||
}
|
||||
|
||||
const accessToken = createAccessToken(user)
|
||||
const refreshToken = createRefreshToken(user)
|
||||
|
||||
user.refreshToken = refreshToken
|
||||
await user.save()
|
||||
|
||||
res.cookie('access_token', accessToken, {
|
||||
httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV !== 'dev', maxAge: 15 * 60 * 1000,
|
||||
})
|
||||
res.cookie('refresh_token', refreshToken, {
|
||||
httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV !== 'dev', maxAge: 7 * 24 * 3600 * 1000,
|
||||
})
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
user: {
|
||||
username: ldapUser.user.cn,
|
||||
email: ldapUser.user.dn
|
||||
},
|
||||
})
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(401).json({ error: 'Invalid credentials' })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/refresh', async (req: Request, res: Response) => {
|
||||
const token = req.cookies.refresh_token
|
||||
if (!token) return res.status(401).json({ error: 'No refresh token' })
|
||||
try {
|
||||
const payload = jwt.verify(token, REFRESH_TOKEN_SECRET)
|
||||
const user = await UserModel.findById(payload.sub)
|
||||
if (!user || !user.refreshToken === token) return res.status(403).json({ error: 'Invalid refresh token' })
|
||||
|
||||
const newAccessToken = createAccessToken(user)
|
||||
const newRefreshToken = createRefreshToken(user)
|
||||
|
||||
user.refreshToken = newRefreshToken
|
||||
await user.save()
|
||||
|
||||
res.cookie('access_token', newAccessToken, {
|
||||
httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV !== 'dev', maxAge: 15 * 60 * 1000,
|
||||
})
|
||||
res.cookie('refresh_token', newRefreshToken, {
|
||||
httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV !== 'dev', maxAge: 7 * 24 * 3600 * 1000,
|
||||
})
|
||||
res.json({ ok: true })
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid refresh token' })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/logout', async (req: Request, res: Response) => {
|
||||
const token = req.cookies.refresh_token
|
||||
if (token) {
|
||||
try {
|
||||
const payload = jwt.verify(token, REFRESH_TOKEN_SECRET)
|
||||
const user = await UserModel.findById(payload.sub)
|
||||
if (user) {
|
||||
user.refreshToken = ''
|
||||
await user.save()
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
res.clearCookie('access_token')
|
||||
res.clearCookie('refresh_token')
|
||||
res.json({ loggedOut: true })
|
||||
})
|
||||
|
||||
export default router as Router
|
||||
31
server/src/api/v1/auth/ldap.ts
Normal file
31
server/src/api/v1/auth/ldap.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
import { Client } from 'ldapts'
|
||||
|
||||
const LDAP_URL = 'ldap://192.168.0.26:389';
|
||||
const BASE_DN = 'DC=ldap,DC=goauthentik,DC=io';
|
||||
|
||||
async function ldapAuth(userOptions: any) {
|
||||
const { username, password } = userOptions;
|
||||
if (!username || !password) return { auth: false }
|
||||
|
||||
const client = new Client({ url: LDAP_URL });
|
||||
|
||||
try {
|
||||
const userDN = `cn=${username},ou=users,${BASE_DN}`;
|
||||
await client.bind(userDN, password);
|
||||
|
||||
const { searchEntries } = await client.search(BASE_DN, {
|
||||
scope: 'sub',
|
||||
filter: `(cn=${username})`,
|
||||
attributes: ['cn', 'mail'],
|
||||
});
|
||||
|
||||
return { auth: true, user: searchEntries[0] }
|
||||
} catch (err) {
|
||||
return { auth: false }
|
||||
} finally {
|
||||
await client.unbind().catch(() => { })
|
||||
}
|
||||
}
|
||||
|
||||
export default ldapAuth
|
||||
@@ -1,12 +1,15 @@
|
||||
import express, { type Router, type Request, type Response } from 'express'
|
||||
import express, { type Router, type Response } from 'express'
|
||||
import { ApiKeyModel } from '../../../models/apikey.model.ts'
|
||||
import { verifyAccessToken, type AuthRequest } from '../../../middleware/auth.ts'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use(verifyAccessToken)
|
||||
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
router.get('/', verifyAccessToken, async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const doc = await ApiKeyModel.findOne({})
|
||||
const userId = req.userId
|
||||
const doc = await ApiKeyModel.findOne({ userId: userId })
|
||||
|
||||
const apiKey = doc?.apiKey || ''
|
||||
|
||||
@@ -17,17 +20,19 @@ router.get('/', async (_req: Request, res: Response) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', async (req: Request, res: Response) => {
|
||||
router.post('/', verifyAccessToken, async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { apiKey } = req.body as { apiKey?: string }
|
||||
const { apiKey } = req.body
|
||||
if (!apiKey || !apiKey.trim()) {
|
||||
return res.status(400).json({ error: 'Invalid API key' })
|
||||
}
|
||||
console.log(req.body)
|
||||
const userId = req.userId
|
||||
|
||||
await ApiKeyModel.updateOne(
|
||||
{},
|
||||
{ $set: { apiKey: apiKey } },
|
||||
{ upsert: true }
|
||||
{ $set: { userId: userId, apiKey: apiKey, lastUsed: new Date() } },
|
||||
{ upsert: true },
|
||||
)
|
||||
|
||||
res.json({ success: true })
|
||||
@@ -37,9 +42,10 @@ router.post('/', async (req: Request, res: Response) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/', async (_req: Request, res: Response) => {
|
||||
router.delete('/', verifyAccessToken, async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const result = await ApiKeyModel.deleteOne({})
|
||||
const userId = req.userId
|
||||
const result = await ApiKeyModel.deleteOne({ userId: userId })
|
||||
|
||||
if (result.deletedCount === 0) {
|
||||
console.log('No API key found to delete.')
|
||||
|
||||
@@ -7,7 +7,6 @@ const router = express.Router()
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const doc = await KanjiModel.find()
|
||||
console.log(doc)
|
||||
res.json(doc)
|
||||
} catch (error) {
|
||||
console.error('Error fetching Kanji Subjects', error)
|
||||
|
||||
22
server/src/api/v1/user/index.ts
Normal file
22
server/src/api/v1/user/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import express, { type Router } from 'express'
|
||||
|
||||
import { UserModel } from '../../../models/user.model.ts'
|
||||
import { verifyAccessToken, type AuthRequest } from '../../../middleware/auth.ts'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/info', verifyAccessToken, async (req: AuthRequest, res) => {
|
||||
try {
|
||||
if (!req.userId) return res.status(401).json({ ok: false, message: 'Unauthorized' })
|
||||
|
||||
const user = await UserModel.findById(req.userId).select('-refreshToken -__v -createdAt -updatedAt')
|
||||
if (!user) return res.status(404).json({ ok: false, message: 'User not found' })
|
||||
|
||||
return res.json({ ok: true, user })
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return res.status(500).json({ ok: false, message: 'Server error' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router as Router
|
||||
@@ -1,26 +1,22 @@
|
||||
import express, { Router } from 'express'
|
||||
import express, { type Router, type Response } from 'express'
|
||||
|
||||
import { ApiKeyModel } from '../../../models/apikey.model.ts'
|
||||
|
||||
import { syncWanikaniData } from '../../../services/wanikaniService.ts'
|
||||
import { verifyAccessToken, type AuthRequest } from '../../../middleware/auth.ts'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
interface ApiKeyDocument {
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
router.get('/sync', async (req, res) => {
|
||||
router.get('/sync', verifyAccessToken, async (req: AuthRequest, res: Response) => {
|
||||
if (!req.userId) return res.status(401).json({ error: 'Unauthorized' })
|
||||
try {
|
||||
const apiKeyDoc = await ApiKeyModel.findOne() as ApiKeyDocument | null
|
||||
const apiKeyDoc = await ApiKeyModel.findOne({ userId: req.userId })
|
||||
|
||||
const apiKey = apiKeyDoc?.apiKey
|
||||
|
||||
console.log(apiKey, apiKeyDoc)
|
||||
if (!apiKey || apiKey.trim() === '') {
|
||||
return res.status(401).json({ error: 'API Key not configured. Please sync your key first.' })
|
||||
}
|
||||
await syncWanikaniData(apiKey)
|
||||
await syncWanikaniData(apiKey, req.userId)
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
export async function connectMongo() {
|
||||
const url = process.env.NODE_ENV === 'DEV' ? 'mongodb://mongo-srs:27017/srs' : 'mongodb://192.168.0.26:27017/srs'
|
||||
const url = process.env.NODE_ENV === 'DEV' ? 'mongodb://mongo:27017/srs' : 'mongodb://192.168.0.26:27017/srs'
|
||||
if (mongoose.connection.readyState === 1) return
|
||||
await mongoose.connect(url)
|
||||
console.log('✅ Connected to MongoDB at', url)
|
||||
|
||||
@@ -1,19 +1,68 @@
|
||||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
import express from 'express'
|
||||
import { connectMongo } from './db/connect.ts'
|
||||
import cors from 'cors'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import helmet from 'helmet'
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
import { connectMongo } from './db/connect.ts'
|
||||
|
||||
import keyRouter from './api/v1/key/index.ts'
|
||||
import syncRouter from './api/v1/wanikani/sync.ts'
|
||||
import kanjiRouter from './api/v1/subject/kanji.ts'
|
||||
import vocabRouter from './api/v1/subject/vocab.ts'
|
||||
import authRouter from './api/v1/auth/index.ts'
|
||||
import userRoutes from './api/v1/user/index.ts'
|
||||
|
||||
import { verifyAccessToken } from './middleware/auth.ts'
|
||||
|
||||
const allowedOrigins = [
|
||||
'http://localhost:5173',
|
||||
'https://srs.crylia.de',
|
||||
]
|
||||
|
||||
const app = express()
|
||||
app.use(cors())
|
||||
app.use(cors({
|
||||
origin: (origin, callback) => {
|
||||
if (!origin) return callback(null, true)
|
||||
if (allowedOrigins.includes(origin)) return callback(null, true)
|
||||
callback(new Error('Not allowed by CORS'))
|
||||
},
|
||||
credentials: true,
|
||||
}))
|
||||
app.use(express.json())
|
||||
app.use(cookieParser())
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
connectSrc: ["'self'", "https://srs.crylia.de"],
|
||||
},
|
||||
},
|
||||
crossOriginEmbedderPolicy: true,
|
||||
crossOriginResourcePolicy: { policy: "same-origin" },
|
||||
})
|
||||
)
|
||||
} else {
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: false,
|
||||
})
|
||||
)
|
||||
}
|
||||
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }))
|
||||
|
||||
app.use('/api/v1/key', keyRouter)
|
||||
app.use('/api/v1/wanikani', syncRouter)
|
||||
app.use('/api/v1/subject/kanji', kanjiRouter)
|
||||
app.use('/api/v1/subject/vocab', vocabRouter)
|
||||
app.use('/api/v1/key', verifyAccessToken, keyRouter)
|
||||
app.use('/api/v1/subject/kanji', verifyAccessToken, kanjiRouter)
|
||||
app.use('/api/v1/subject/vocab', verifyAccessToken, vocabRouter)
|
||||
app.use('/api/v1/user', verifyAccessToken, userRoutes)
|
||||
app.use('/api/v1/wanikani', verifyAccessToken, syncRouter)
|
||||
app.use('/api/v1/auth', authRouter)
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
connectMongo().then(() => {
|
||||
|
||||
21
server/src/middleware/auth.ts
Normal file
21
server/src/middleware/auth.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { type Request, type Response, type NextFunction } from 'express'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
userId?: string
|
||||
}
|
||||
|
||||
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!
|
||||
|
||||
export function verifyAccessToken(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
const token = req.cookies.access_token
|
||||
if (!token) return res.status(401).json({ ok: false, message: 'No token provided' })
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(token, ACCESS_TOKEN_SECRET!)
|
||||
req.userId = (payload as any).sub
|
||||
next()
|
||||
} catch {
|
||||
return res.status(401).json({ ok: false, message: 'Invalid token' })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
const ApiKeySchema = new mongoose.Schema({
|
||||
userId: { type: String, required: true },
|
||||
apiKey: { type: String, required: true },
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
const AssignmentSchema = new mongoose.Schema({
|
||||
userId: { type: String, required: true },
|
||||
subject_type: String,
|
||||
subject_ids: [Number],
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import mongoose from 'mongoose'
|
||||
import type { KanjiItem } from '../types/wanikani.ts'
|
||||
|
||||
const KanjiSchema = new mongoose.Schema<KanjiItem>({
|
||||
userId: { type: String, required: true },
|
||||
characters: String,
|
||||
meanings: Array,
|
||||
readings: Array,
|
||||
|
||||
9
server/src/models/user.model.ts
Normal file
9
server/src/models/user.model.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const UserSchema = new mongoose.Schema({
|
||||
username: { type: String, unique: true, required: true },
|
||||
email: { type: String, unique: true },
|
||||
refreshToken: String,
|
||||
}, { timestamps: true })
|
||||
|
||||
export const UserModel = mongoose.model('User', UserSchema)
|
||||
@@ -2,6 +2,7 @@ import mongoose from 'mongoose'
|
||||
import type { VocabularyItem } from '../types/wanikani.ts'
|
||||
|
||||
const VocabSchema = new mongoose.Schema<VocabularyItem>({
|
||||
userId: { type: String, required: true },
|
||||
characters: String,
|
||||
meanings: Array,
|
||||
readings: Array,
|
||||
|
||||
@@ -63,7 +63,8 @@ const fetchSubjects = async (
|
||||
return results
|
||||
}
|
||||
|
||||
const mapKanji = (item: WaniKaniSubject): KanjiItem => ({
|
||||
const mapKanji = (item: WaniKaniSubject, userId: string): KanjiItem => ({
|
||||
userId: userId,
|
||||
characters: item.data.characters,
|
||||
meanings: item.data.meanings,
|
||||
readings: item.data.readings,
|
||||
@@ -73,7 +74,8 @@ const mapKanji = (item: WaniKaniSubject): KanjiItem => ({
|
||||
srs_score: 0,
|
||||
})
|
||||
|
||||
const mapVocab = (item: WaniKaniSubject): VocabularyItem => ({
|
||||
const mapVocab = (item: WaniKaniSubject, userId: string): VocabularyItem => ({
|
||||
userId: userId,
|
||||
characters: item.data.characters,
|
||||
meanings: item.data.meanings,
|
||||
readings: item.data.readings ?? [],
|
||||
@@ -84,13 +86,13 @@ const mapVocab = (item: WaniKaniSubject): VocabularyItem => ({
|
||||
srs_score: 0,
|
||||
})
|
||||
|
||||
export const syncWanikaniData = async (apiKey: string): Promise<void> => {
|
||||
export const syncWanikaniData = async (apiKey: string, userId: string): Promise<void> => {
|
||||
const headers = { Authorization: `Bearer ${apiKey}` }
|
||||
|
||||
try {
|
||||
await ApiKeyModel.updateOne(
|
||||
{},
|
||||
{ $set: { value: apiKey, lastUsed: new Date() } },
|
||||
{ $set: { user: userId, apiKey: apiKey, lastUsed: new Date() } },
|
||||
{ upsert: true },
|
||||
)
|
||||
|
||||
@@ -112,13 +114,13 @@ export const syncWanikaniData = async (apiKey: string): Promise<void> => {
|
||||
}
|
||||
|
||||
await AssignmentModel.updateOne(
|
||||
{ subject_type: 'kanji' },
|
||||
{ userId: userId, subject_type: 'kanji' },
|
||||
{ $set: { subject_ids: unlockedKanjiSubjectIds } },
|
||||
{ upsert: true },
|
||||
)
|
||||
|
||||
await AssignmentModel.updateOne(
|
||||
{ subject_type: 'vocabulary' },
|
||||
{ userId: userId, subject_type: 'vocabulary' },
|
||||
{ $set: { subject_ids: unlockedVocabSubjectIds } },
|
||||
{ upsert: true },
|
||||
)
|
||||
@@ -126,12 +128,12 @@ export const syncWanikaniData = async (apiKey: string): Promise<void> => {
|
||||
const existingKanjiSlugs = new Set((await KanjiModel.find({}, { slug: 1 })).map(k => k.slug))
|
||||
const kanjiSubjects = await fetchSubjects(unlockedKanjiSubjectIds, headers)
|
||||
const newKanji = kanjiSubjects.filter(s => !existingKanjiSlugs.has(s.data.slug))
|
||||
if (newKanji.length > 0) await KanjiModel.insertMany(newKanji.map(mapKanji))
|
||||
if (newKanji.length > 0) await KanjiModel.insertMany(newKanji.map(k => mapKanji(k, userId)))
|
||||
|
||||
const existingVocabSlugs = new Set((await VocabularyModel.find({}, { slug: 1 })).map(v => v.slug))
|
||||
const vocabSubjects = await fetchSubjects(unlockedVocabSubjectIds, headers)
|
||||
const newVocab = vocabSubjects.filter(s => !existingVocabSlugs.has(s.data.slug))
|
||||
if (newVocab.length > 0) await VocabularyModel.insertMany(newVocab.map(mapVocab))
|
||||
if (newVocab.length > 0) await VocabularyModel.insertMany(newVocab.map(v => mapVocab(v, userId)))
|
||||
|
||||
console.log('✅ Sync complete')
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface KanjiItem {
|
||||
userId: string
|
||||
characters: string
|
||||
meanings: {
|
||||
meaning: string,
|
||||
@@ -21,6 +22,7 @@ export interface KanjiItem {
|
||||
}
|
||||
|
||||
export interface VocabularyItem {
|
||||
userId: string
|
||||
characters: string
|
||||
meanings: {
|
||||
meaning: string
|
||||
@@ -51,6 +53,7 @@ export interface VocabularyItem {
|
||||
}
|
||||
|
||||
export interface Assignment {
|
||||
userId: string
|
||||
unlocked_at?: Date
|
||||
subject_ids: number[]
|
||||
subject_type: string
|
||||
|
||||
Reference in New Issue
Block a user