diff --git a/package-lock.json b/package-lock.json index b164cb6..ac8424d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,8 @@ "passport-local": "^1.0.0", "passport-openidconnect": "^0.1.2", "pg-promise": "^11.10.1", - "typed-rest-client": "^2.1.0" + "typed-rest-client": "^2.1.0", + "uuid": "^11.0.2" }, "devDependencies": { "@types/express": "^5.0.0", @@ -2251,6 +2252,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", + "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index 117890f..e12368e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "passport-local": "^1.0.0", "passport-openidconnect": "^0.1.2", "pg-promise": "^11.10.1", - "typed-rest-client": "^2.1.0" + "typed-rest-client": "^2.1.0", + "uuid": "^11.0.2" }, "devDependencies": { "@types/express": "^5.0.0", diff --git a/src/index.ts b/src/index.ts index 5178af5..da01499 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import pgPromise from "pg-promise"; import rateLimit from "express-rate-limit"; import helmet from "helmet"; import dotenv from "dotenv"; +import { v4 as uuidv4 } from 'uuid'; import * as httpm from 'typed-rest-client/HttpClient'; dotenv.config(); @@ -40,14 +41,23 @@ passport.use(new JwtStrat({ }, async (jwt_payload: any, done: any) => { console.log('jwt payload', jwt_payload); const user = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', jwt_payload.id); - console.log('jwt user', user) + + console.log('jwt user', user); if (user) { + const impersonationId = await db.oneOrNone('SELECT "targetId" FROM "Impersonation" WHERE "sourceId" = $1', jwt_payload.id); + if (impersonationId) { + const impersonation = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', impersonationId.targetId); + if (impersonation) { + user.impersonation = impersonation; + console.log('found impersonation via jwt'); + } + } done(null, user); } else { done(null, false); } })); -const session = require('express-session') +const session = require('express-session'); const OpenIDConnectStrategy = require('passport-openidconnect'); app.use(session({ key: 'passport', @@ -81,6 +91,7 @@ passport.use(new OpenIDConnectStrategy({ const impersonation = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', impersonationId.targetId); if (impersonation) { user.impersonation = impersonation; + console.log('found impersonation via open id'); } } return done(null, user); @@ -103,12 +114,9 @@ app.get('/api/auth', passport.authenticate("openidconnect", { failureRedirect: ' res.send(''); }); -app.get('/api/auth/jwt', passport.authenticate("jwt"), (req: Request, res: Response) => { - res.send({ authenticated: true }); -}); - -app.get('/api/loggedin', (req: any, res: Response) => { - res.send(['test test test ', req.user ? 'yes' : 'no']); +app.get('/api/auth/validate', [isApiKeyAuthenticated, isJWTAuthenticated], (req: any, res: Response, next: () => void) => { + const user = req?.user; + res.send({ authenticated: user != null, user: user }); }); async function isApiKeyAuthenticated(req: any, res: any, next: any) { @@ -116,19 +124,21 @@ async function isApiKeyAuthenticated(req: any, res: any, next: any) { if (key && !req.user) { const data = await db.oneOrNone('SELECT "userId" from "ApiKey" WHERE id = $1', key); if (data) { - console.log(data); const user = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', data.userId); - const impersonationId = await db.oneOrNone('SELECT "targetId" FROM "Impersonation" WHERE "sourceId" = $1', data.userId); - if (impersonationId) { - const impersonation = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', impersonationId.targetId); - if (impersonation) { - user.impersonation = impersonation; + if (user.role == "ADMIN") { + const impersonationId = await db.oneOrNone('SELECT "targetId" FROM "Impersonation" WHERE "sourceId" = $1', data.userId); + if (impersonationId) { + const impersonation = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', impersonationId.targetId); + if (impersonation) { + user.impersonation = impersonation; + console.log('found impersonation via api key'); + } } } req.user = user } } - next() + next(); } function isWebAuthenticated(req: any, res: any, next: () => void) { @@ -140,16 +150,103 @@ function isWebAuthenticated(req: any, res: any, next: () => void) { res.status(401).send({ message: 'User is not authenticated.' }); } -const apiMiddlewares = [isApiKeyAuthenticated, passport.authenticate('jwt', { session: false }), isWebAuthenticated] +function isJWTAuthenticated(req: any, res: any, next: () => void) { + if (req.user) { + next(); + return; + } + + const check = passport.authenticate('jwt', { session: false }); + check(req, res, next); +} + +const apiMiddlewares = [isApiKeyAuthenticated, isJWTAuthenticated, isWebAuthenticated] + +app.get('/api/admin/users', apiMiddlewares, async (req: any, res: any, next: any) => { + if (req.user.role != 'ADMIN') { + res.status(403).send('You do not have the permissions for this.'); + return; + } + + const data = await db.manyOrNone('SELECT id, name FROM "User"'); + res.send(data); +}); + +app.put('/api/admin/impersonate', apiMiddlewares, async (req: any, res: any, next: any) => { + if (req.user.role != 'ADMIN') { + res.status(403).send('You do not have the permissions for this.'); + return; + } + + if (!req.body.impersonation) { + res.status(400).send('Invalid user.'); + return; + } + + const impersonation = await db.one('SELECT EXISTS (SELECT 1 FROM "User" WHERE id = $1)', req.body.impersonation); + if (!impersonation) { + res.status(400).send('Invalid user.'); + return; + } + + const data = await db.oneOrNone('SELECT "targetId" FROM "Impersonation" where "sourceId" = $1', req.user.id); + if (!data?.targetId) { + const insert = await db.none('INSERT INTO "Impersonation" ("sourceId", "targetId") VALUES ($1, $2)', [req.user.id, req.body.impersonation]); + res.send(insert); + } else { + const update = await db.none('UPDATE "Impersonation" SET "targetId" = $2 WHERE "sourceId" = $1', [req.user.id, req.body.impersonation]); + res.send(update); + } +}); + +app.delete('/api/admin/impersonate', apiMiddlewares, async (req: any, res: any, next: any) => { + if (req.user.role != 'ADMIN') { + res.status(403).send('You do not have the permissions for this.'); + return; + } + + const data = await db.oneOrNone('DELETE FROM "Impersonation" where "sourceId" = $1', req.user.id); + res.send(data); +}); app.get('/api/keys', apiMiddlewares, async (req: any, res: any, next: any) => { - const userId = req.user.id; + const userId = req.user.impersonation?.id ?? req.user.id; const data = await db.manyOrNone('SELECT id, label FROM "ApiKey" WHERE "userId" = $1', userId); res.send(data); }); +app.post('/api/keys', apiMiddlewares, async (req: any, res: any, next: any) => { + const userId = req.user.impersonation?.id ?? req.user.id; + const keys = await db.one('SELECT count(*) FROM "ApiKey" WHERE "userId" = $1', userId); + if (keys.count > 10) { + res.status(400).send('too many keys'); + return; + } + const label = req.body.label; + if (!label) { + res.status(400).send('no label is attached.'); + return; + } + const key = uuidv4(); + await db.none('INSERT INTO "ApiKey" (id, label, "userId") VALUES ($1, $2, $3);', [key, label, userId]); + res.send({ label, key }); +}); + +app.delete('/api/keys', apiMiddlewares, async (req: any, res: any, next: any) => { + if (!req.body.key) { + res.status(400).send('key has not been provided.'); + return; + } + const key = await db.one('SELECT EXISTS(SELECT 1 FROM "ApiKey" WHERE id = $1)', req.body.key); + if (!key.exists) { + res.status(400).send('key does not exist.'); + return; + } + res.send({ key: req.body.key }); +}); + app.post("/api/auth/twitch/callback", async (req: any, res: any) => { - console.log(req.headers['user-agent']) + console.log(req.headers['user-agent']); const query = `client_id=${process.env.AUTH_CLIENT_ID}&client_secret=${process.env.AUTH_CLIENT_SECRET}&code=${req.body.code}&grant_type=authorization_code&redirect_uri=${process.env.AUTH_REDIRECT_URI}` const rest = new httpm.HttpClient(null); const response = await rest.post('https://id.twitch.tv/oauth2/token', query, { @@ -162,7 +259,7 @@ app.post("/api/auth/twitch/callback", async (req: any, res: any) => { res.send({ authenticated: false }); return; } - console.log('Successfully validated Twitch code authentication. Attempting to read user data from Twitch.') + console.log('Successfully validated Twitch code authentication. Attempting to read user data from Twitch.'); const resp = await rest.get('https://api.twitch.tv/helix/users', { 'Authorization': 'Bearer ' + data.access_token, @@ -171,16 +268,15 @@ app.post("/api/auth/twitch/callback", async (req: any, res: any) => { const b = await resp.readBody(); const twitch = JSON.parse(b); if (!twitch?.data) { + console.log('Failed to fetch twitch data:', twitch?.data); res.send({ authenticated: false }); return; } - console.log('twitch data', twitch.data[0]) - const account: any = await db.oneOrNone('SELECT "userId" FROM "Account" WHERE "providerAccountId" = $1', twitch.data[0].id); if (account != null) { const user: any = await db.one('SELECT id FROM "User" WHERE id = $1', account.userId); - console.log('userrrr', user) + console.log('User fetched successfully:', user.id); const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '30d' }); res.send({ authenticated: true, token: token }); diff --git a/src/middleware/auth0.middleware.ts b/src/middleware/auth0.middleware.ts deleted file mode 100644 index e69de29..0000000