diff --git a/data/oauth.ts b/data/oauth.ts new file mode 100644 index 0000000..aa38c88 --- /dev/null +++ b/data/oauth.ts @@ -0,0 +1,34 @@ +export const OAuthData: { [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: process.env.WEB_HOST + '/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: process.env.WEB_HOST + '/connections/callback' + }, +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 367f3e0..aae4728 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "express-session": "^1.18.1", "helmet": "^8.0.0", "jsonwebtoken": "^9.0.2", + "moment": "^2.30.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", @@ -1336,6 +1337,15 @@ "node": "*" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/package.json b/package.json index e12368e..e9baf64 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "main": "src/index.js", "scripts": { "build": "npx tsc", - "start": "node dist/index.js", + "start": "node dist/src/index.js", "dev": "nodemon src/index.ts" }, "keywords": [], @@ -18,6 +18,7 @@ "express-session": "^1.18.1", "helmet": "^8.0.0", "jsonwebtoken": "^9.0.2", + "moment": "^2.30.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", diff --git a/src/controllers/admin.controller.ts b/src/controllers/admin.controller.ts new file mode 100644 index 0000000..3aee756 --- /dev/null +++ b/src/controllers/admin.controller.ts @@ -0,0 +1,35 @@ +import { database } from "../services/database"; + +export async function getUsers(req: any, res: any, next: any) { + const users = await database.manyOrNone('SELECT id, name FROM "User"'); + res.send(users); +}; + +export async function updateImpersonation(req: any, res: any, next: any) { + if (!req.body.impersonation) { + res.status(400).send('Invalid user.'); + return; + } + + const data = await database.oneOrNone('SELECT "targetId" FROM "Impersonation" where "sourceId" = $1', req.user.id); + if (!data?.targetId) { + await database.none('INSERT INTO "Impersonation" ("sourceId", "targetId") VALUES ($1, $2)', [req.user.id, req.body.impersonation]); + } else { + await database.none('UPDATE "Impersonation" SET "targetId" = $2 WHERE "sourceId" = $1', [req.user.id, req.body.impersonation]); + } + res.send({ + data: { + impersonation: true + } + }); +}; + +export async function deleteImpersonation(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 database.oneOrNone('DELETE FROM "Impersonation" where "sourceId" = $1', req.user.id); + res.send({ data }); +}; \ No newline at end of file diff --git a/src/controllers/api-keys.controller.ts b/src/controllers/api-keys.controller.ts new file mode 100644 index 0000000..f3d9d6c --- /dev/null +++ b/src/controllers/api-keys.controller.ts @@ -0,0 +1,41 @@ +import { database } from "../services/database"; +import { v4 as uuidv4 } from 'uuid'; + +export async function getApiKeys(req: any, res: any, next: any) { + const userId = req.user.impersonation?.id ?? req.user.id; + const data = await database.manyOrNone('SELECT id, label FROM "ApiKey" WHERE "userId" = $1', userId); + res.send(data); +}; + +export async function createApiKey(req: any, res: any, next: any) { + const userId = req.user.impersonation?.id ?? req.user.id; + const keys = await database.one('SELECT count(*) FROM "ApiKey" WHERE "userId" = $1', userId); + if (keys.count > 10) { + res.status(403).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 database.none('INSERT INTO "ApiKey" (id, label, "userId") VALUES ($1, $2, $3);', [key, label, userId]); + res.send({ label, key }); +}; + +export async function deleteApiKey(req: any, res: any, next: any) { + if (!req.body.key) { + res.status(400).send('key has not been provided.'); + return; + } + const key = await database.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; + } + + await database.none('DELETE FROM "ApiKey" WHERE id = $1', req.body.key); + res.send({ key: req.body.key }); +}; \ No newline at end of file diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts new file mode 100644 index 0000000..ed0d9f3 --- /dev/null +++ b/src/controllers/auth.controller.ts @@ -0,0 +1,121 @@ +import { Request, Response } from "express"; +import { database } from "../services/database"; +import * as http from 'typed-rest-client/HttpClient'; +import { jwt } from "../services/passport"; +import { OAuthData } from "../../data/oauth"; +import { v4 as uuidv4 } from 'uuid'; +import moment from "moment"; +const pino = require('pino-http')(); + + +export async function connectionPreparation(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 database.oneOrNone('SELECT "name", "type", "clientId", "grantType", "userId" FROM "ConnectionState" WHERE "state" = $1', [state]); + if (!connection) { + res.status(400).send({ error: 'Invalid combination.' }); + return; + } + + if (connection.type == 'twitch') { + const rest = new http.HttpClient(null); + const response = await rest.get('https://id.twitch.tv/oauth2/validate', { + 'Authorization': 'OAuth ' + access_token, + }); + const body = await response.readBody(); + const json = JSON.parse(body); + expires_in = json.expires_in; + + if (!expires_in) { + res.status(400).send({ error: 'Twitch API is not working correctly.' }); + return; + } + } + + const expiration = expires_in ? parseInt(expires_in.toString()) : null; + if (!expiration || isNaN(expiration) || !isFinite(expiration) || expiration < 0) { + res.status(400).send({ error: 'Could not determine the expiration of the token.' }); + return; + } + + const expires_at = moment().add(expiration - 300, 's'); + res.send({ + data: { connection, expires_at }, + }); +}; + +export async function connectionCallback(req: any, res: Response) { + 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) { + res.status(400).send({ error: 'Missing fields in the body.' }); + return; + } + + const url = OAuthData[type].endpoint; + const redirect = OAuthData[type].redirect; + const scopes = OAuthData[type].scopes.join(' '); + const nounce = uuidv4(); + await database.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 }); +}; + +// login/register +export async function twitchCallback(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 http.HttpClient(null); + const response = await rest.post('https://id.twitch.tv/oauth2/token', query, { + 'Content-Type': 'application/x-www-form-urlencoded' + }); + const body = await response.readBody(); + const codeData = JSON.parse(body); + if (!codeData || codeData.message) { + console.log('Failed to validate Twitch code authentication:', codeData); + res.send({ authenticated: false }); + return; + } + console.log('Validated Twitch code authentication:', codeData); + + const resp = await rest.get('https://api.twitch.tv/helix/users', { + 'Authorization': 'Bearer ' + codeData.access_token, + 'Client-Id': process.env.AUTH_CLIENT_ID + }); + const b = await resp.readBody(); + const userData = JSON.parse(b); + if (!userData?.data) { + console.log('Failed to fetch twitch data:', codeData, userData?.data); + res.send({ authenticated: false, error: 'Twitch API is not working correctly.' }); + return; + } + console.log('Fetched Twitch user data:', userData); + + const account: any = await database.oneOrNone('SELECT "userId" FROM "Account" WHERE "providerAccountId" = $1', userData.data[0].id); + if (account != null) { + const user: any = await database.one('SELECT id FROM "User" WHERE id = $1', account.userId); + const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '30d' }); + res.send({ authenticated: true, token: token }); + + var now = Date.now(); + const expires_at = ((now / 1000) | 0) + codeData.expires_in; + await database.none('UPDATE "Account" SET refresh_token = COALESCE($1, refresh_token), access_token = $2, id_token = COALESCE($3, id_token), expires_at = $4, scope = $5 WHERE "userId" = $6', [codeData.refresh_token, codeData.access_token, codeData.id_token, expires_at, codeData.scope.join(' '), account.userId]); + return; + } + + res.send({ authenticated: false }); +}; + +export async function validate(req: any, res: Response, next: () => void) { + res.send({ authenticated: req?.user != null, user: req?.user }); +} \ No newline at end of file diff --git a/src/controllers/twitch.controller.ts b/src/controllers/twitch.controller.ts new file mode 100644 index 0000000..916dc41 --- /dev/null +++ b/src/controllers/twitch.controller.ts @@ -0,0 +1,48 @@ +import { database } from "../services/database"; +import * as http from 'typed-rest-client/HttpClient'; + +export async function getTwitchRedemptions(req: any, res: any, next: any) { + const userId = req.user.impersonation?.id ?? req.user.id; + const account: any = await database.one('SELECT "providerAccountId" FROM "Account" WHERE "userId" = $1', userId); + const connection: any = await database.oneOrNone('SELECT "clientId", "accessToken" FROM "Connection" WHERE "userId" = $1 AND "default" = true AND "type" = \'twitch\'', userId); + const rest = new http.HttpClient(null); + const resp = await rest.get('https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=' + account.providerAccountId, { + 'Authorization': 'Bearer ' + connection.accessToken, + 'Client-Id': connection.clientId, + }); + + const twitch = JSON.parse(await resp.readBody()); + if (!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; + } + + res.send(twitch.data); +}; + +export async function getTwitchUsers(req: any, res: any) { + const username = req.query.login?.toLowerCase(); + if (!username) { + res.status(400).send({ user: null }); + return; + } + + const rest = new http.HttpClient(null); + const userId = req.user.impersonation?.id ?? req.user.id; + + const connection: any = await database.oneOrNone('SELECT "clientId", "accessToken" FROM "Connection" WHERE "userId" = $1 AND "default" = true AND "type" = \'twitch\'', userId); + + const resp = await rest.get('https://api.twitch.tv/helix/users?login=' + username, { + 'Authorization': 'Bearer ' + connection.accessToken, + 'Client-Id': connection.clientId, + }); + const twitch = JSON.parse(await resp.readBody()); + if (!twitch?.data) { + res.status(403).send({ user: null }); + return; + } + + const user = twitch.data.find((u: any) => u.login == username); + res.send({ user }); +}; \ No newline at end of file diff --git a/src/database.postgres.sql b/src/database.postgres.sql new file mode 100644 index 0000000..d289000 --- /dev/null +++ b/src/database.postgres.sql @@ -0,0 +1,244 @@ +DELETE TYPE IF EXISTS "UserRole"; + +DELETE TYPE IF EXISTS "ActionType"; + +DROP TABLE IF EXISTS "actions"; + +DROP TABLE IF EXISTS "chatters"; + +DROP TABLE IF EXISTS "chatter_groups"; + +DROP TABLE IF EXISTS "redemptions"; + +DROP TABLE IF EXISTS "permissions"; + +DROP TABLE IF EXISTS "policies"; + +DROP TABLE IF EXISTS "groups"; + +DROP TABLE IF EXISTS "connections"; + +DROP TABLE IF EXISTS "accounts"; + +DROP TABLE IF EXISTS "api_keys"; + +DROP TABLE IF EXISTS "users"; + +CREATE TYPE "UserRole" AS ENUM ('USER', 'ADMIN'); + +CREATE TYPE "ActionType" AS ENUM ( + 'WRITE_TO_FILE', + 'APPEND_TO_FILE', + 'AUDIO_FILE', + 'OBS_TRANSFORM', + 'RANDOM_TTS_VOICE', + 'SPECIFIC_TTS_VOICE', + 'TTS_MESSAGE', + 'TOGGLE_OBS_VISIBILITY', + 'SPECIFIC_OBS_VISIBILITY', + 'SPECIFIC_OBS_INDEX', + 'SLEEP', + 'OAUTH', + 'NIGHTBOT_PLAY', + 'NIGHTBOT_PAUSE', + 'NIGHTBOT_SKIP', + 'TWITCH_OAUTH', + 'NIGHTBOT_CLEAR_PLAYLIST', + 'NIGHTBOT_CLEAR_QUEUE', + 'VEADOTUBE_SET_STATE', + 'VEADOTUBE_PUSH_STATE', + 'VEADOTUBE_POP_STATE' +); + +CREATE TABLE + users ( + user_id uuid DEFAULT gen_random_uuid (), + name text NOT NULL, + email text, + role "UserRole" NOT NULL DEFAULT USER, + image text, + tts_default_voice text NOT NULL, + created_at timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp(3) NOT NULL, + CONSTRAINT "users_pkey" PRIMARY KEY ("user_id") + ); + +CREATE TABLE + accounts ( + account_id uuid DEFAULT gen_random_uuid (), + user_id uuid NOT NULL, + type text NOT NULL, + provider text NOT NULL, + providerAccountId text NOT NULL, + access_token text NOT NULL, + refresh_token text NOT NULL, + expires_at integer NOT NULL, + token_type text NOT NULL, + scope text NOT NULL, + created_at timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "accounts_pkey" PRIMARY KEY ("account_id"), + CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") + ); + +CREATE TABLE + actions ( + user_id uuid DEFAULT gen_random_uuid (), + name text NOT NULL, + type "ActionType" NOT NULL, + data jsonb NOT NULL, + has_message boolean NOT NULL DEFAULT false, + CONSTRAINT "Action_pkey" PRIMARY KEY ("user_id", "name"), + CONSTRAINT "s_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); + +CREATE TABLE + api_keys ( + user_id uuid NOT NULL, + key text NOT NULL, + label text NOT NULL, + CONSTRAINT "api_keys_pkey" PRIMARY KEY ("key") CONSTRAINT "api_keys_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); + +CREATE TABLE + chatters (chatter_id text NOT NULL, name text NOT NULL); + +CREATE TABLE + chatter_groups ( + chatter_id text DEFAULT gen_random_uuid (), + group_id uuid DEFAULT gen_random_uuid (), + user_id uuid NOT NULL, + chatter_label text NOT NULL, + CONSTRAINT "chatter_groups_pkey" PRIMARY KEY ("user_id", "group_id", "chatter_id") CONSTRAINT "chatter_groups_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); + +CREATE TABLE + connections ( + user_id uuid NOT NULL, + name text NOT NULL, + provider text NOT NULL, + grant_type text NOT NULL, + client_id text NOT NULL, + access_token NOT NULL, + scope text NOT NULL, + expires_at timestamp(3) NOT NULL, + default boolean NOT NULL DEFAULT false, + CONSTRAINT "connections_pkey" PRIMARY KEY ("user_id", "name") CONSTRAINT "connections_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); + +CREATE TABLE + connection_previews ( + user_id uuid NOT NULL, + name text NOT NULL, + provider text NOT NULL, + grant_type text NOT NULL, + client_id text NOT NULL, + state text NOT NULL, + CONSTRAINT "connection_previews_pkey" PRIMARY KEY ("user_id", "name") CONSTRAINT "connection_previews_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); + +CREATE TABLE + emotes ( + emote_id text NOT NULL, + label text NOT NULL, + CONSTRAINT "emotes_pkey" PRIMARY KEY ("emote_id") + ); + +CREATE TABLE + emote_usages ( + emote_id text NOT NULL, + chatter_id text NOT NULL, + broadcaster_id text NOT NULL, + timestamp timestamp(3) NOT NULL + ); + +CREATE TABLE + groups ( + group_id uuid DEFAULT gen_random_uuid (), + user_id uuid NOT NULL, + name text NOT NULL, + priority integer NOT NULL, + CONSTRAINT "groups_pkey" PRIMARY KEY ("group_id"), + CONSTRAINT "groups_user_id_name_unique" UNIQUE ("user_id", "name") CONSTRAINT "groups_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); + +CREATE INDEX "groups_userId_idx" ON "groups" USING btree ("user_id"); + +CREATE TABLE + permissions ( + permission_id uuid DEFAULT gen_random_uuid (), + group_id uuid NOT NULL, + user_id uuid NOT NULL, + path text NOT NULL, + allow boolean, + CONSTRAINT "permissions" PRIMARY KEY ("permission_id") CONSTRAINT "permissions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); + +CREATE INDEX "permissions_user_id_idx" ON "permissions" USING btree ("user_id"); + +CREATE TABLE + policies ( + policy_id uuid DEFAULT gen_random_uuid (), + group_id uuid NOT NULL, + user_id uuid NOT NULL, + path text NOT NULL, + count integer NOT NULL, + span integer NOT NULL, + CONSTRAINT "policies_pkey" PRIMARY KEY ("policy_id") CONSTRAINT "policies_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); + +CREATE INDEX "policies_user_id_idx" ON "policies" USING btree ("user_id"); + +CREATE TABLE + impersonations ( + source_id uuid NOT NULL, + target_id uuid NOT NULL, + CONSTRAINT "impersonations_pkey" PRIMARY KEY ("source_id") CONSTRAINT "impersonations_source_id_fkey" FOREIGN KEY ("source_id") REFERENCES users ("user_id") ON DELETE CASCADE CONSTRAINT "impersonations_target_id_fkey" FOREIGN KEY ("target_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); + +CREATE TABLE + redemption_actions ( + redemption_actions_id uuid DEFAULT gen_random_uuid (), + user_id uuid NOT NULL, + provider_redemption_id text, + action_name text NOT NULL, + order integer NOT NULL, + state boolean NOT NULL, + CONSTRAINT "redemption_actions_pkey" PRIMARY KEY ("redemption_actions_id"), + CONSTRAINT "redemptions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); + +CREATE TABLE + tts_chatter_voices ( + chatter_id text NOT NULL, + user_id uuid NOT NULL, + tts_voice_id uuid NOT NULL, + CONSTRAINT "tts_chatter_voices" PRIMARY KEY ("user_id", "chatter_id") CONSTRAINT "tts_chatter_voices_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); + +CREATE TABLE + tts_voices ( + tts_voice_id uuid DEFAULT gen_random_uuid (), + name uuid NOT NULL, + gender text, + base_language text CONSTRAINT "tts_voices_pkey" PRIMARY KEY ("tts_voice_id"), + ); + +CREATE TABLE + tts_voice_states ( + tts_voice_id uuid NOT NULL, + user_id NOT NULL, + state boolean NOT NULL DEFAULT true, + CONSTRAINT "tts_voice_states_pkey" PRIMARY KEY ("tts_voice_id", "user_id"), + CONSTRAINT "tts_voice_states_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); + +CREATE TABLE + tts_word_filters ( + tts_word_filter_id uuid DEFAULT gen_random_uuid (), + user_id NOT NULL, + search text NOT NULL, + replace text NOT NULL, + flag integer NOT NULL, + CONSTRAINT "tts_word_filters_pkey" PRIMARY KEY ("tts_word_filter_id"), + CONSTRAINT "tts_word_filters_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES users ("user_id") ON DELETE CASCADE + ); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index e559f4e..d036fbb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,25 +1,19 @@ -import express, { Express, Request, Response } from "express"; -import pgPromise from "pg-promise"; +import express, { Express, Request } from "express"; 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(); - if (!process.env.CONNECTION_STRING) { throw new Error("Cannot find connection string."); } - -const pgp = pgPromise({}); -const db = pgp(process.env.CONNECTION_STRING as string); +import { passport } from "./services/passport"; const limiter = rateLimit({ legacyHeaders: true, standardHeaders: true, - windowMs: 15 * 60 * 1000, - limit: 200, + windowMs: 15 * 1000, + limit: 8, max: 2, message: "Too many requests, please try again later.", keyGenerator: (req: Request) => req.ip as string, @@ -28,395 +22,32 @@ const limiter = rateLimit({ const app: Express = express(); const port = process.env.PORT || 3000; -app.use(express.json()); -app.use(express.urlencoded()); - -var jwt = require('jsonwebtoken'); -const passport = require('passport'); -const JwtStrat = require('passport-jwt').Strategy; -const ExtractJwt = require('passport-jwt').ExtractJwt; -passport.use(new JwtStrat({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: process.env.JWT_SECRET, -}, async (jwt_payload: any, done: any) => { - const user = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', jwt_payload.id); - - if (user) { - done(null, user); - } else { - done(null, false); - } -})); const session = require('express-session'); -const OpenIDConnectStrategy = require('passport-openidconnect'); app.use(session({ - key: 'passport', - secret: process.env.AUTH_SECRET, + secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, + cookie: { + maxAge: 7 * 24 * 60 * 60 * 1000, + secure: true, + } })); app.use(passport.initialize()); app.use(passport.session()); -app.set('trust proxy', true); - -passport.use(new OpenIDConnectStrategy({ - issuer: 'https://id.twitch.tv/oauth2', - authorizationURL: 'https://id.twitch.tv/oauth2/authorize', - tokenURL: 'https://id.twitch.tv/oauth2/token', - clientID: process.env.AUTH_CLIENT_ID, - clientSecret: process.env.AUTH_CLIENT_SECRET, - callbackURL: process.env.AUTH_REDIRECT_URI, - scope: 'user_read' -}, async (url: any, profile: any, something: any, done: any) => { - console.log('login', 'pus:', profile, url, something); - const account: any = await db.oneOrNone('SELECT "userId" FROM "Account" WHERE "providerAccountId" = $1', profile.id); - if (account != null) { - const user: any = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', account.userId); - if (user.name != profile.username) { - db.none('UPDATE "User" SET name = $1 WHERE id = $2', [profile.username, profile.id]); - user.name = profile.username; - } - return done(null, user); - } - return done(new Error('Account does not exist.'), null); -} -)); - -passport.serializeUser((user: any, done: any) => { - if (!user) - return done(new Error('user is null'), null); - return done(null, user); -}); - -passport.deserializeUser((user: any, done: any) => { - done(null, user); -}); - -app.get('/api/auth', passport.authenticate("openidconnect", { failureRedirect: '/login' }), (req: Request, res: Response) => { - res.send(''); -}); - -app.get('/api/auth/validate', [isApiKeyAuthenticated, isJWTAuthenticated, updateImpersonation], (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) { - if (!req.user) { - const key = req.get('x-api-key'); - if (key) { - const data = await db.oneOrNone('SELECT "userId" from "ApiKey" WHERE id = $1', key); - if (data) { - req.user = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', data.userId); - } - } - } - next(); -} - -function isNotAuthenticated(req: any, res: any, next: () => void) { - if (req.user) { - next(); - return; - } - - console.log('user is not authenticated.'); - res.status(401).send({ message: 'User is not authenticated.' }); -} - -function isJWTAuthenticated(req: any, res: any, next: () => void) { - if (req.user) { - next(); - return; - } - - const check = passport.authenticate('jwt', { session: false }); - check(req, res, next); -} - -async function updateImpersonation(req: any, res: any, next: () => void) { - if (req.user && req.user.role == 'ADMIN') { - const impersonationId = await db.oneOrNone('SELECT "targetId" FROM "Impersonation" WHERE "sourceId" = $1', req.user.id); - if (impersonationId) { - const impersonation = await db.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', impersonationId.targetId); - if (impersonation) { - req.user.impersonation = impersonation; - } - } - } - next(); -} - -const apiMiddlewares = [isApiKeyAuthenticated, isJWTAuthenticated, updateImpersonation, isNotAuthenticated] - -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) { - await db.none('INSERT INTO "Impersonation" ("sourceId", "targetId") VALUES ($1, $2)', [req.user.id, req.body.impersonation]); - } else { - await db.none('UPDATE "Impersonation" SET "targetId" = $2 WHERE "sourceId" = $1', [req.user.id, req.body.impersonation]); - } - res.send(); -}); - -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.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(403).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; - } - - await db.none('DELETE FROM "ApiKey" WHERE id = $1', req.body.key); - res.send({ key: req.body.key }); -}); - -app.get('/api/twitch/redemptions', apiMiddlewares, async (req: any, res: any, next: any) => { - const userId = req.user.impersonation?.id ?? req.user.id; - const account: any = await db.one('SELECT "providerAccountId" FROM "Account" WHERE "userId" = $1', userId); - const connection: any = await db.oneOrNone('SELECT "clientId", "accessToken" FROM "Connection" WHERE "userId" = $1 AND "default" = true AND "type" = \'twitch\'', userId); - const rest = new httpm.HttpClient(null); - const resp = await rest.get('https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id=' + account.providerAccountId, { - 'Authorization': 'Bearer ' + connection.accessToken, - 'Client-Id': connection.clientId, - }); - - const twitch = JSON.parse(await resp.readBody()); - if (!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; - } - - res.send(twitch.data); -}); - -app.get("/api/auth/twitch/users", apiMiddlewares, async (req: any, res: any) => { - const username = req.query.login?.toLowerCase(); - if (!username) { - res.status(400).send({ user: null }); - return; - } - - const rest = new httpm.HttpClient(null); - const userId = req.user.impersonation?.id ?? req.user.id; - - const connection: any = await db.oneOrNone('SELECT "clientId", "accessToken" FROM "Connection" WHERE "userId" = $1 AND "default" = true AND "type" = \'twitch\'', userId); - - const resp = await rest.get('https://api.twitch.tv/helix/users?login=' + username, { - 'Authorization': 'Bearer ' + connection.accessToken, - 'Client-Id': connection.clientId, - }); - const twitch = JSON.parse(await resp.readBody()); - if (!twitch?.data) { - res.status(403).send({ user: null }); - return; - } - - const user = twitch.data.find((u: any) => u.login == username); - 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); - const response = await rest.post('https://id.twitch.tv/oauth2/token', query, { - 'Content-Type': 'application/x-www-form-urlencoded' - }); - const body = await response.readBody(); - const data = JSON.parse(body); - if (!data || data.message) { - console.log('Failed to validate Twitch code authentication:', data); - res.send({ authenticated: false }); - return; - } - 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, - 'Client-Id': process.env.AUTH_CLIENT_ID - }); - const b = await resp.readBody(); - const twitch = JSON.parse(b); - if (!twitch?.data) { - console.log('Failed to fetch twitch data:', data, twitch?.data); - res.send({ authenticated: false }); - return; - } - - 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); - const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '30d' }); - res.send({ authenticated: true, token: token }); - - var now = Date.now(); - const expires_at = ((now / 1000) | 0) + data.expires_in; - await db.none('UPDATE "Account" SET refresh_token = COALESCE($1, refresh_token), access_token = $2, id_token = COALESCE($3, id_token), expires_at = $4, scope = $5 WHERE "userId" = $6', [data.refresh_token, data.access_token, data.id_token, expires_at, data.scope.join(' '), account.userId]); - return; - } - - res.send({ authenticated: false }); -}); +const cors = require("cors"); +app.use(cors({ credentials: true, origin: true })); +app.use(express.json()); +app.use(express.urlencoded()); app.use(helmet()); app.use(limiter); +app.set('trust proxy', true); + +app.get('/api/auth', passport.authenticate("openidconnect", { failureRedirect: '/login' })); +app.use(require('./routes/admin.route')); +app.use(require('./routes/api-keys.route')); +app.use(require('./routes/auth.route')); +app.use(require('./routes/twitch.route')); app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); diff --git a/src/middlewares/api-key.middleware.ts b/src/middlewares/api-key.middleware.ts new file mode 100644 index 0000000..8efff2b --- /dev/null +++ b/src/middlewares/api-key.middleware.ts @@ -0,0 +1,25 @@ +import { database } from "../services/database"; +import { passport } from "../services/passport"; + +export async function isApiKeyAuthenticated(req: any, res: any, next: any) { + if (!req.user) { + const key = req.get('x-api-key'); + if (key) { + const data = await database.oneOrNone('SELECT "userId" from "ApiKey" WHERE id = $1', key); + if (data) { + req.user = await database.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', data.userId); + } + } + } + next(); +} + +export function isJWTAuthenticated(req: any, res: any, next: () => void) { + if (req.user) { + next(); + return; + } + + const check = passport.authenticate('jwt', { session: false }); + check(req, res, next); +} \ No newline at end of file diff --git a/src/middlewares/authentication.middleware.ts b/src/middlewares/authentication.middleware.ts new file mode 100644 index 0000000..2c11ff6 --- /dev/null +++ b/src/middlewares/authentication.middleware.ts @@ -0,0 +1,32 @@ +export function isAuthenticated(req: any, res: any, next: () => void) { + if (req.user) { + next(); + return; + } + + res.status(401).send({ error: 'User is not authenticated.' }); +} + +export function isAdminAuthenticated(req: any, res: any, next: () => void) { + if (req.user) { + if (req.user.role == 'ADMIN') { + next(); + return; + } + + res.status(403).send('You do not have the permissions for this.'); + return; + } + + res.status(401).send({ error: 'User is not authenticated.' }); +} + + +export function isNotAuthenticated(req: any, res: any, next: () => void) { + if (!req.user) { + next(); + return; + } + + res.status(403).send({ error: 'User is authenticated.' }); +} \ No newline at end of file diff --git a/src/middlewares/common.middleware.ts b/src/middlewares/common.middleware.ts new file mode 100644 index 0000000..8cada0e --- /dev/null +++ b/src/middlewares/common.middleware.ts @@ -0,0 +1,8 @@ +import { isApiKeyAuthenticated, isJWTAuthenticated } from "./api-key.middleware"; +import { isAdminAuthenticated, isAuthenticated, isNotAuthenticated } from "./authentication.middleware"; +import { checkImpersonation } from "./impersonation.middleware"; + +export const AUTH_MIDDLEWARES = [isJWTAuthenticated, checkImpersonation]; +export const PUBLIC_API_MIDDLEWARES = []; +export const PROTECTED_API_MIDDLEWARES = [isApiKeyAuthenticated, isJWTAuthenticated, checkImpersonation, isAuthenticated]; +export const ADMIN_API_MIDDLEWARES = [isApiKeyAuthenticated, isJWTAuthenticated, isAdminAuthenticated] \ No newline at end of file diff --git a/src/middlewares/impersonation.middleware.ts b/src/middlewares/impersonation.middleware.ts new file mode 100644 index 0000000..9b2ea63 --- /dev/null +++ b/src/middlewares/impersonation.middleware.ts @@ -0,0 +1,14 @@ +import { database } from "../services/database"; + +export async function checkImpersonation(req: any, res: any, next: () => void) { + if (req.user && req.user.role == 'ADMIN') { + const impersonationId = await database.oneOrNone('SELECT "targetId" FROM "Impersonation" WHERE "sourceId" = $1', req.user.id); + if (impersonationId) { + const impersonation = await database.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', impersonationId.targetId); + if (impersonation) { + req.user.impersonation = impersonation; + } + } + } + next(); +} \ No newline at end of file diff --git a/src/routes/admin.route.ts b/src/routes/admin.route.ts new file mode 100644 index 0000000..7c4e639 --- /dev/null +++ b/src/routes/admin.route.ts @@ -0,0 +1,12 @@ +import { ADMIN_API_MIDDLEWARES } from "../middlewares/common.middleware"; + +export { }; +const express = require('express'); +const router = express.Router(); +const adminController = require('../controllers/admin.controller'); + +router.get('/api/admin/users', ADMIN_API_MIDDLEWARES, adminController.getUsers); +router.put('/api/admin/impersonate', ADMIN_API_MIDDLEWARES, adminController.updateImpersonation); +router.delete('/api/admin/impersonate', ADMIN_API_MIDDLEWARES, adminController.deleteImpersonation); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/api-keys.route.ts b/src/routes/api-keys.route.ts new file mode 100644 index 0000000..cf98ed8 --- /dev/null +++ b/src/routes/api-keys.route.ts @@ -0,0 +1,12 @@ +import { PROTECTED_API_MIDDLEWARES } from "../middlewares/common.middleware"; + +export { }; +const express = require('express'); +const router = express.Router(); +const keyController = require('../controllers/api-keys.controller'); + +router.get('/api/keys', PROTECTED_API_MIDDLEWARES, keyController.getApiKeys); +router.post('/api/keys', PROTECTED_API_MIDDLEWARES, keyController.createApiKey); +router.delete('/api/keys', PROTECTED_API_MIDDLEWARES, keyController.deleteApiKey); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/auth.route.ts b/src/routes/auth.route.ts new file mode 100644 index 0000000..ede1ff7 --- /dev/null +++ b/src/routes/auth.route.ts @@ -0,0 +1,13 @@ +import { AUTH_MIDDLEWARES, PROTECTED_API_MIDDLEWARES, PUBLIC_API_MIDDLEWARES } from "../middlewares/common.middleware"; + +export { }; +const express = require('express'); +const router = express.Router(); +const authController = require('../controllers/auth.controller'); + +router.get('/api/auth/connections', authController.connectionCallback); +router.post('/api/auth/connections', PROTECTED_API_MIDDLEWARES, authController.connectionPreparation); +router.post('/api/auth/twitch/callback', authController.twitchCallback); +router.get('/api/auth/validate', AUTH_MIDDLEWARES, authController.validate); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/twitch.route.ts b/src/routes/twitch.route.ts new file mode 100644 index 0000000..071531e --- /dev/null +++ b/src/routes/twitch.route.ts @@ -0,0 +1,11 @@ +import { PROTECTED_API_MIDDLEWARES } from "../middlewares/common.middleware"; + +export { }; +const express = require('express'); +const router = express.Router(); +const twitchController = require('../controllers/twitch.controller'); + +router.get('/api/twitch/redemptions', PROTECTED_API_MIDDLEWARES, twitchController.getTwitchRedemptions); +router.get('/api/twitch/users', PROTECTED_API_MIDDLEWARES, twitchController.getTwitchUsers); + +module.exports = router; \ No newline at end of file diff --git a/src/services/database.ts b/src/services/database.ts new file mode 100644 index 0000000..fe1d864 --- /dev/null +++ b/src/services/database.ts @@ -0,0 +1,4 @@ +import pgPromise from "pg-promise"; + +const pgp = pgPromise(); +export const database = pgp(process.env.CONNECTION_STRING!); \ No newline at end of file diff --git a/src/services/passport.ts b/src/services/passport.ts new file mode 100644 index 0000000..c624631 --- /dev/null +++ b/src/services/passport.ts @@ -0,0 +1,56 @@ +import { OAuthData } from "../../data/oauth"; +import { database } from "./database"; + +export const jwt = require('jsonwebtoken'); +export const passport = require('passport'); + +const JwtStrat = require('passport-jwt').Strategy; +const ExtractJwt = require('passport-jwt').ExtractJwt; +passport.use(new JwtStrat({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: process.env.JWT_SECRET, +}, async (jwt_payload: any, done: any) => { + const user = await database.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', jwt_payload.id); + + if (user) { + done(null, user); + } else { + done(null, false); + } +})); + +const OpenIDConnectStrategy = require('passport-openidconnect'); + +passport.use(new OpenIDConnectStrategy({ + issuer: 'https://id.twitch.tv/oauth2', + authorizationURL: 'https://id.twitch.tv/oauth2/authorize', + tokenURL: 'https://id.twitch.tv/oauth2/token', + clientID: process.env.AUTH_CLIENT_ID, + clientSecret: process.env.AUTH_CLIENT_SECRET, + callbackURL: process.env.AUTH_REDIRECT_URI, + scope: 'user_read ' + OAuthData['twitch'].scopes.join(' '), +}, + async (url: any, profile: any, something: any, done: any) => { + console.log('login', 'profile:', profile, 'url', url, 'something', something); + const account: any = await database.oneOrNone('SELECT "userId" FROM "Account" WHERE "providerAccountId" = $1', profile.id); + if (account != null) { + const user: any = await database.oneOrNone('SELECT id, name, role, "ttsDefaultVoice" FROM "User" WHERE id = $1', account.userId); + if (user.name != profile.username) { + await database.none('UPDATE "User" SET name = $1 WHERE id = $2', [profile.username, profile.id]); + user.name = profile.username; + } + return done(null, user); + } + return done(new Error('Account does not exist.'), null); + } +)); + +passport.serializeUser((user: any, done: any) => { + if (!user) + return done(new Error('user is null'), null); + return done(null, user); +}); + +passport.deserializeUser((user: any, done: any) => { + done(null, user); +}); \ No newline at end of file