diff --git a/lib/Database.ts b/lib/Database.ts index 83a1aa7..9b5470d 100644 --- a/lib/Database.ts +++ b/lib/Database.ts @@ -1,58 +1,8 @@ -import { TeamRole } from '@/utils/permissions'; import mysql, { Pool, QueryResult } from 'mysql2/promise'; -import crypto from 'node:crypto'; - -interface DiscordTokenData { - token_type: string; - access_token: string; - expires_in: number; - refresh_token: string; - scope: string; - id_token: string; -} - -interface UserTable { - id: number; - did: string; - username: string; - global_name: string; - email: string; - avatar?: string; - banner?: string; - accent_color?: number; - permissions: number; -} - -interface UserTableWithoutEmail { - id: number; - did: string; - username: string; - global_name: string; - avatar?: string; - banner?: string; - accent_color?: number; - permissions: number; -} - -interface TeamMember { - id: number; - uid: number; - did: string; - username: string; - global_name: string; - email: string; - avatar: string; - banner: string; - accent_color: number; - permissions: number; - role: TeamRole; -} - class Database { - static instance: (Database | null) = null; - - mysqlPool: (Pool | null) = null; + static instance: (Database | null) = null; + private mysqlPool: (Pool | null) = null; constructor() { if(Database.instance) @@ -71,154 +21,6 @@ class Database { Database.instance = this; } - - // Auth Stuff - - async createUser(user: any): Promise { - const [ result ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE did = ?', [ user.id ]); - - if ((result as any).length > 0) { - return (result as any)[0].id; - } - - const [ res ] = await this.mysqlPool!.execute('INSERT INTO users (did, username, global_name, email, avatar, banner, accent_color) VALUES (?, ?, ?, ?, ?, ?, ?)', [ - user.id, - user.username, - (user.global_name || user.username), - user.email, - user.avatar, - user.banner, - user.accent_color - ]); - - return (res as any).insertId; - } - - /** - * Creates a session for the user. - * - * Should only be called by backend! - * - * @param userData The data returned from the `/oauth2/token` endpoint. - * @param userAgent The user agent of the user. - * - * @returns `string` The session token. - */ - async createSession(userData: DiscordTokenData, userAgent?: string): Promise { - const res = await fetch(`${process.env.DISCORD_API}/users/@me`, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${userData.access_token}`, - }, - }); - - if (res.ok) { - const user = await res.json(); - const uid = await this.createUser(user); - - const sum = crypto.createHmac('sha256', process.env.AUTH_SECRET!); - const base = Buffer.from(user.id).toString('base64').replaceAll('=', ''); - const date = Buffer.from((Date.now() - 1688940000000).toString()).toString('base64').replaceAll('=', ''); - - sum.update(userData.access_token); - - const sid = base + '.' + date + '.' + sum.digest('hex'); - - const [ result ] = await this.mysqlPool!.execute('INSERT INTO sessions (sid, uid, access_token, refresh_token, id_token, user_agent, expires) VALUES (?, ?, ?, ?, ?, ?, ?)', [ - sid, - uid, - userData.access_token, - userData.refresh_token, - userData.id_token, - userAgent, - new Date(Date.now() + (userData.expires_in * 1000)), - ]); - - return sid; - } else { - throw new Error('Error Fetching Discord User Data'); - } - } - - async deleteSession(sid: string): Promise { - await this.mysqlPool!.execute('DELETE FROM sessions WHERE sid = ?', [ sid ]); - } - - async getSession(sid: string): Promise { - const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM sessions WHERE sid = ?', [ sid ]); - - return (rows as any)[0]; - } - - // End of Auth Stuff - - async getUser(id: number): Promise { - const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE id = ?', [ id ]); - - return (rows as UserTable[])[0]; - } - - async getUsers(): Promise { - const [ rows ] = await this.mysqlPool!.execute('SELECT id, did, username, global_name, avatar, banner, accent_color, permissions FROM users'); - - return rows as UserTableWithoutEmail[]; - } - - async getUserUsername(username: string): Promise { - const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE username = ?', [ username ]); - - return (rows as UserTable[])[0]; - } - - async getUserSessions(uid: number): Promise { - const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM sessions WHERE uid = ?', [ uid ]); - - return rows; - } - - async getTeam(): Promise { - const [ rows ] = await this.mysqlPool!.execute('SELECT team_members.id AS tid, team_members.uid AS uid, users.did, users.username, users.global_name, users.email, users.avatar, users.banner, users.accent_color, users.permissions, team_members.role FROM team_members JOIN users ON team_members.uid = users.id;'); - - return (rows as any).map((row: any) => { - return { - id: row.tid, - uid: row.uid, - did: row.did, - username: row.username, - global_name: row.global_name, - email: row.email, - avatar: row.avatar, - banner: row.banner, - accent_color: row.accent_color, - permissions: row.permissions, - role: (row.role as TeamRole), - }; - }); - } - - async getNations(): Promise { - const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM nations'); - - return rows; - } - - async getNationCode(code: string): Promise { - const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM nations WHERE code = ?', [ code ]); - - return (rows as any)[0]; - } - - async getCompaies(nid: number): Promise { - const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM companies WHERE nid = ?', [ nid ]); - - return rows; - } - - async updateUserPermissions(id: number, permissions: any) { - await this.mysqlPool!.execute('UPDATE users SET permissions = ? WHERE id = ?', [ permissions, id ]); - - return; - } } export default Database; \ No newline at end of file diff --git a/lib/Discord.ts b/lib/Discord.ts new file mode 100644 index 0000000..c0d5b41 --- /dev/null +++ b/lib/Discord.ts @@ -0,0 +1,22 @@ +class Discord { + private token: string; + + constructor(token: string) { + this.token = token; + } + + async user(): Promise<{ ok: boolean, data?: any }> { + const res = await fetch(`${process.env.DISCORD_API}/users/@me`, { + headers: { + Authorization: `Bearer ${this.token}`, + } + }); + + return { + ok: res.ok, + data: res.ok ? await res.json() : null, + }; + } +} + +export default Discord; \ No newline at end of file diff --git a/package.json b/package.json index d45713d..a5a60a7 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "lucide-react": "^0.429.0", "mysql2": "^3.11.0", "next": "14.2.6", + "nodejs-snowflake": "^2.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "showdown": "^2.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e00179e..e5b12fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: next: specifier: 14.2.6 version: 14.2.6(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) + nodejs-snowflake: + specifier: ^2.0.1 + version: 2.0.1 react: specifier: ^18.3.1 version: 18.3.1 @@ -2470,6 +2473,9 @@ packages: node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + nodejs-snowflake@2.0.1: + resolution: {integrity: sha512-zMfDorNNsJm1OWWx/OUUGVT0bQ3TwC2ti4URD8UjnpffVLtLpy3eBDIaV5TnLs91YrRhwBD6L0StqU6YtnZt9A==} + nopt@5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} engines: {node: '>=6'} @@ -5525,7 +5531,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -5556,7 +5562,7 @@ snapshots: is-bun-module: 1.1.0 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -5574,7 +5580,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -6260,6 +6266,8 @@ snapshots: node-releases@2.0.18: {} + nodejs-snowflake@2.0.1: {} + nopt@5.0.0: dependencies: abbrev: 1.1.1 diff --git a/utils/snowflake.ts b/utils/snowflake.ts new file mode 100644 index 0000000..72829df --- /dev/null +++ b/utils/snowflake.ts @@ -0,0 +1,8 @@ +import { Snowflake } from 'nodejs-snowflake'; + +const snowflake = new Snowflake({ + custom_epoch: new Date('2023-07-10T00:00:00Z').getTime(), + instance_id: 1 +}); + +export default snowflake; \ No newline at end of file diff --git a/utils/token_util.ts b/utils/token_util.ts new file mode 100644 index 0000000..6b73fc5 --- /dev/null +++ b/utils/token_util.ts @@ -0,0 +1,52 @@ +import crypto from 'node:crypto'; + +import snowflake from './snowflake'; + +import Discord from '@/lib/Discord'; + +interface DiscordTokenData { + token_type: string; + access_token: string; + expires_in: number; + refresh_token: string; + scope: string; + id_token: string; +}; + +/** + * Creates a session token. + * + * @param oauthData The data returned from the `/oauth2/token` endpoint. + * + * @returns The session token. + */ +export async function createSessionToken(oauthData: DiscordTokenData): Promise { + const discord = new Discord(oauthData.access_token); + const res = await discord.user(); + + if (!res.ok) + throw new Error('Failed to fetch user data from Discord.'); + + const dcUser = res.data; + + const hmac = crypto + .createHmac('sha256', process.env.AUTH_SECRET!) + .update(dcUser.access_token) + .digest('hex'); + + const tuid = Buffer + .from(dcUser.id) + .toString('base64') + .replaceAll('=', ''); + + const tdid = Buffer + .from( + snowflake + .getUniqueID() + .toString() + ) + .toString('base64') + .replaceAll('=', ''); + + return `${tuid}.${tdid}.${hmac}`; +} \ No newline at end of file