diff --git a/src/index.ts b/src/index.ts index ee13da0..e559f4e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -205,12 +205,12 @@ 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'); + res.status(403).send('Too many keys'); return; } const label = req.body.label; if (!label) { - res.status(400).send('no label is attached.'); + res.status(400).send('No label is attached.'); return; } const key = uuidv4(); @@ -228,6 +228,8 @@ app.delete('/api/keys', apiMiddlewares, async (req: any, res: any, next: any) => res.status(400).send('key does not exist.'); return; } + + await db.none('DELETE FROM "ApiKey" WHERE id = $1', req.body.key); res.send({ key: req.body.key }); }); @@ -243,7 +245,7 @@ app.get('/api/twitch/redemptions', apiMiddlewares, async (req: any, res: any, ne const twitch = JSON.parse(await resp.readBody()); if (!twitch?.data) { - console.log('Failed to fetch twitch data:', account, twitch?.data); + console.log('Failed to fetch twitch data:', account, twitch.data); res.status(401).send({ error: 'Could not fetch Twitch channel redemption data.' }); return; } @@ -252,9 +254,9 @@ app.get('/api/twitch/redemptions', apiMiddlewares, async (req: any, res: any, ne }); app.get("/api/auth/twitch/users", apiMiddlewares, async (req: any, res: any) => { - const username = req.query.login.toLowerCase(); + const username = req.query.login?.toLowerCase(); if (!username) { - res.send({ user: null }); + res.status(400).send({ user: null }); return; } @@ -269,7 +271,7 @@ app.get("/api/auth/twitch/users", apiMiddlewares, async (req: any, res: any) => }); const twitch = JSON.parse(await resp.readBody()); if (!twitch?.data) { - res.send({ user: null }); + res.status(403).send({ user: null }); return; } @@ -277,6 +279,100 @@ app.get("/api/auth/twitch/users", apiMiddlewares, async (req: any, res: any) => res.send({ user }); }); +app.post("/api/auth/connections", apiMiddlewares, async (req: any, res: any) => { + const name = req.body.name; + const type = req.body.type?.toLowerCase(); + const client_id = req.body.client_id; + const grant_type = req.body.grant_type?.toLowerCase(); + if (!name || !type || !client_id || !grant_type) { + const missing = [name, type, client_id, grant_type] + res.status(400).send({ error: 'Missing fields in the body.' }); + return; + } + + const AuthData: { [service: string]: { type: string, endpoint: string, grantType: string, scopes: string[], redirect: string } } = { + 'nightbot': { + type: 'nightbot', + endpoint: 'https://api.nightbot.tv/oauth2/authorize', + grantType: 'token', + scopes: ['song_requests', 'song_requests_queue', 'song_requests_playlist'], + redirect: 'https://beta.tomtospeech.com/connections/callback' + }, + 'twitch': { + type: 'twitch', + endpoint: 'https://id.twitch.tv/oauth2/authorize', + grantType: 'token', + scopes: [ + 'chat:read', + 'bits:read', + 'channel:read:polls', + 'channel:read:predictions', + 'channel:read:subscriptions', + 'channel:read:vips', + 'moderator:read:blocked_terms', + 'chat:read', + 'channel:moderate', + 'channel:read:redemptions', + 'channel:manage:redemptions', + 'channel:manage:predictions', + 'user:read:chat', + 'channel:bot', + 'moderator:read:followers', + 'channel:read:ads', + 'moderator:read:chatters', + ], + redirect: 'https://beta.tomtospeech.com/connections/callback' + }, + }; + + const url = AuthData[type].endpoint; + const redirect = AuthData[type].redirect; + const scopes = AuthData[type].scopes.join(' '); + const nounce = uuidv4(); + await db.none('INSERT INTO "ConnectionState" ("name", "type", "clientId", "grantType", "state", "userId") VALUES ($1, $2, $3, $4, $5, $6)' + + ' ON CONFLICT ("userId", "name") DO UPDATE SET "type" = $2, "clientId" = $3, "grantType" = $4, "state" = $5;', + [name, type, client_id, grant_type, nounce, req.user.id]); + + const redirect_uri = url + '?client_id=' + client_id + '&force_verify=true&redirect_uri=' + redirect + '&response_type=token&scope=' + scopes + '&state=' + nounce; + res.send({ success: true, error: null, data: redirect_uri }); +}); + +app.get("/api/auth/connections", async (req: Request, res: Response) => { + const state = req.query['state']; + const access_token = req.query['token']; + let expires_in = req.query['expires_in']; + if (!state || !access_token) { + res.status(400).send({ error: 'Missing fields in the body.' }); + return; + } + + const connection = await db.oneOrNone('SELECT "name", "type", "clientId", "grantType", "userId" FROM "ConnectionState" WHERE "state" = $1', [state]); + if (!connection) { + res.status(400).send({ error: 'Failed to link the account.' }); + return; + } + + if (connection.type == 'twitch') { + const rest = new httpm.HttpClient(null); + const response = await rest.get('https://id.twitch.tv/oauth2/validate', { + 'Authorization': 'OAuth ' + access_token, + }); + const json = JSON.parse(await response.readBody()); + expires_in = json.expires_in; + } + + if (!expires_in) { + res.status(400).send({ error: 'Could not determine the expiration of the token.' }); + return; + } + + const expires_at = new Date(); + expires_at.setSeconds(expires_at.getSeconds() + parseInt(expires_in.toString()) - 300); + res.send({ + data: { connection, expires_at }, + }); +}); + app.post("/api/auth/twitch/callback", async (req: any, res: any) => { 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);