Compare commits
20 commits
Author | SHA1 | Date | |
---|---|---|---|
TheClashFruit | a4173e24ba | ||
TheClashFruit | d9377a9731 | ||
TheClashFruit | e4012844a6 | ||
TheClashFruit | a9f943b7e4 | ||
TheClashFruit | 89a31da575 | ||
TheClashFruit | c6881a16da | ||
TheClashFruit | 4e7c2a1df2 | ||
TheClashFruit | 0a8b61c472 | ||
TheClashFruit | bd27f0084c | ||
TheClashFruit | 36ac57378a | ||
TheClashFruit | 57e5f69bc8 | ||
TheClashFruit | 6c8793ff68 | ||
TheClashFruit | 54cf25a554 | ||
TheClashFruit | 1dde9b1a84 | ||
TheClashFruit | d6db115c08 | ||
TheClashFruit | dbbefde41e | ||
TheClashFruit | ada5cdd47c | ||
TheClashFruit | a5aae6e9df | ||
TheClashFruit | 638452eb0b | ||
TheClashFruit | 9d6e607ab5 |
|
@ -11,5 +11,12 @@ DB_HOST=
|
|||
DB_PORT=
|
||||
|
||||
AUTH_SECRET=
|
||||
LINK_SECRET=
|
||||
|
||||
MC_API=
|
||||
|
||||
S3_REGION="fra1"
|
||||
S3_ENDPOINT="https://fra1.digitaloceanspaces.com"
|
||||
|
||||
S3_ACCESS_KEY=
|
||||
S3_SECRET_KEY=
|
|
@ -3,7 +3,8 @@
|
|||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
2,
|
||||
{ "SwitchCase": 1 }
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
|
|
|
@ -8,6 +8,7 @@ on:
|
|||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Deploy Over SSH
|
||||
uses: https://github.com/nekiro/ssh-job@main
|
||||
|
|
25
docs/cdn.md
Normal file
25
docs/cdn.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# CDN Url Structure
|
||||
|
||||
Base URL: `https://crss.fra1.cdn.digitaloceanspaces.com/`
|
||||
|
||||
## Users
|
||||
|
||||
### Avatars
|
||||
|
||||
`users/[id]/avatar/[hash].png`
|
||||
|
||||
### Banners
|
||||
|
||||
`users/[id]/banner/[hash].png`
|
||||
|
||||
## Nations
|
||||
|
||||
### Flags
|
||||
|
||||
`nations/[id]/flag/[hash].svg`
|
||||
|
||||
## Gallery
|
||||
|
||||
### Images
|
||||
|
||||
`gallery/[id]/[hash].png`
|
161
docs/database.sql
Normal file
161
docs/database.sql
Normal file
|
@ -0,0 +1,161 @@
|
|||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS `crss` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
USE `crss`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `governments` (
|
||||
`id` bigint(20) NOT NULL,
|
||||
`nation_id` bigint(20) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `nation_id` (`nation_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `government_officials` (
|
||||
`id` bigint(20) NOT NULL,
|
||||
`government_id` bigint(20) NOT NULL,
|
||||
`user_id` bigint(20) NOT NULL,
|
||||
`role_id` bigint(20) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `government_id` (`government_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `role_id` (`role_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `government_roles` (
|
||||
`id` bigint(20) NOT NULL,
|
||||
`government_id` bigint(20) NOT NULL,
|
||||
`level` int(11) NOT NULL,
|
||||
`name` varchar(48) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `government_id` (`government_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `images` (
|
||||
`id` bigint(20) NOT NULL,
|
||||
`user_id` bigint(20) NOT NULL,
|
||||
`name` varchar(64) NOT NULL,
|
||||
`alt` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `image_user` (`user_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `image_locations` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`image_id` bigint(20) NOT NULL,
|
||||
`x` int(11) NOT NULL,
|
||||
`y` int(11) NOT NULL,
|
||||
`z` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `image_id` (`image_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `image_sizes` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`image_id` bigint(20) NOT NULL,
|
||||
`width` int(11) NOT NULL,
|
||||
`height` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `image_id` (`image_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `mc_links` (
|
||||
`id` bigint(20) NOT NULL,
|
||||
`user_id` bigint(20) NOT NULL,
|
||||
`uuid` text NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `mc_link_codes` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) NOT NULL,
|
||||
`code` varchar(8) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `code` (`code`),
|
||||
KEY `user_id` (`user_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `nations` (
|
||||
`id` bigint(20) NOT NULL,
|
||||
`code` varchar(3) NOT NULL,
|
||||
`name` varchar(48) NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `nation_points` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`nation_id` bigint(20) NOT NULL,
|
||||
`x` int(11) NOT NULL,
|
||||
`z` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `nation_id` (`nation_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
`id` bigint(20) NOT NULL,
|
||||
`username` varchar(32) NOT NULL,
|
||||
`display_name` varchar(64) NOT NULL,
|
||||
`email` varchar(320) NOT NULL,
|
||||
`avatar` text NOT NULL,
|
||||
`banner` text NOT NULL,
|
||||
`accent_color` int(11) NOT NULL,
|
||||
`discord_id` bigint(20) NOT NULL,
|
||||
`permissions` int(11) NOT NULL,
|
||||
`badges` int(11) NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`),
|
||||
UNIQUE KEY `email` (`email`),
|
||||
UNIQUE KEY `discord_id` (`discord_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `user_sessions` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) NOT NULL,
|
||||
`token` varchar(1024) NOT NULL,
|
||||
`dc_access_token` text NOT NULL,
|
||||
`dc_refresh_token` text NOT NULL,
|
||||
`dc_id_token` text NOT NULL,
|
||||
`user_agent` text NOT NULL,
|
||||
`ip` text NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`expires_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `token` (`token`) USING HASH,
|
||||
KEY `user_id` (`user_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `user_settings` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) NOT NULL,
|
||||
`settings` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`settings`)),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`)
|
||||
);
|
||||
|
||||
--- Foreign keys
|
||||
|
||||
ALTER TABLE `governments` ADD CONSTRAINT `nation` FOREIGN KEY (`nation_id`) REFERENCES `nations` (`id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `government_officials` ADD CONSTRAINT `government_official` FOREIGN KEY (`government_id`) REFERENCES `governments` (`id`) ON DELETE CASCADE, ADD CONSTRAINT `government_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, ADD CONSTRAINT `role` FOREIGN KEY (`role_id`) REFERENCES `government_roles` (`id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `government_roles` ADD CONSTRAINT `government_role` FOREIGN KEY (`government_id`) REFERENCES `governments` (`id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `images` ADD CONSTRAINT `image_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `image_locations` ADD CONSTRAINT `image_location_id` FOREIGN KEY (`image_id`) REFERENCES `images` (`id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `image_sizes` ADD CONSTRAINT `image_size` FOREIGN KEY (`image_id`) REFERENCES `images` (`id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `mc_links` ADD CONSTRAINT `mc_link_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `mc_link_codes` ADD CONSTRAINT `mc_link_code_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `nation_points` ADD CONSTRAINT `nation_point_id` FOREIGN KEY (`nation_id`) REFERENCES `nations` (`id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `user_sessions` ADD CONSTRAINT `session_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `user_settings` ADD CONSTRAINT `setting_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
|
||||
|
||||
COMMIT;
|
15
interfaces/company.ts
Normal file
15
interfaces/company.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { User } from './user';
|
||||
|
||||
export interface Company {
|
||||
id: BigInt;
|
||||
|
||||
owners: User[];
|
||||
|
||||
name: string;
|
||||
slogan: string;
|
||||
|
||||
logo: string;
|
||||
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
24
interfaces/gallery.ts
Normal file
24
interfaces/gallery.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { Point3D } from './spacial_data';
|
||||
import { User } from './user';
|
||||
|
||||
export interface AspectRatio {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
id: BigInt;
|
||||
|
||||
by: User;
|
||||
|
||||
name: string;
|
||||
alt: string;
|
||||
|
||||
location?: Point3D;
|
||||
|
||||
type: 'image/jpeg' | 'image/png';
|
||||
aspectRatio: AspectRatio;
|
||||
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
30
interfaces/government.ts
Normal file
30
interfaces/government.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { Point2D } from './spacial_data';
|
||||
import { User } from './user';
|
||||
|
||||
export enum GovernmentLevel {
|
||||
Highest,
|
||||
High,
|
||||
Regular,
|
||||
Low,
|
||||
Lowest
|
||||
}
|
||||
|
||||
export interface GovernmentRole {
|
||||
id: BigInt;
|
||||
|
||||
level: GovernmentLevel;
|
||||
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface GovernmentUser extends User {
|
||||
role: GovernmentRole;
|
||||
ordering: number;
|
||||
}
|
||||
|
||||
export interface Government {
|
||||
id: BigInt;
|
||||
|
||||
parliament: GovernmentUser[];
|
||||
// parliamentBulding: Point2D;
|
||||
}
|
12
interfaces/index.ts
Normal file
12
interfaces/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export type * from './user';
|
||||
export type * from './nation';
|
||||
export type * from './government';
|
||||
export type * from './company';
|
||||
export type * from './gallery';
|
||||
|
||||
export type * from './spacial_data';
|
||||
export type * from './minecraft';
|
||||
|
||||
export type * from './settings';
|
||||
|
||||
export type * from './responses';
|
21
interfaces/minecraft.ts
Normal file
21
interfaces/minecraft.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Point3D } from './spacial_data';
|
||||
|
||||
export enum MinecraftDimension {
|
||||
Overworld = 'minecraft:overworld',
|
||||
Nether = 'minecraft:the_nether',
|
||||
End = 'minecraft:the_end',
|
||||
}
|
||||
|
||||
export interface MinecraftPosition extends Point3D {
|
||||
yaw: number;
|
||||
pitch: number;
|
||||
|
||||
dimension: MinecraftDimension;
|
||||
}
|
||||
|
||||
export interface MinecraftPlayer {
|
||||
uuid: string;
|
||||
username: string;
|
||||
|
||||
position: MinecraftPosition;
|
||||
}
|
18
interfaces/nation.ts
Normal file
18
interfaces/nation.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Company } from './company';
|
||||
import { Government } from './government';
|
||||
import { Point2D } from './spacial_data';
|
||||
|
||||
export interface Nation {
|
||||
id: BigInt;
|
||||
code: string;
|
||||
|
||||
name: string;
|
||||
government: Government;
|
||||
|
||||
companies: Company[];
|
||||
|
||||
borderPoints: Point2D[];
|
||||
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
4
interfaces/responses.ts
Normal file
4
interfaces/responses.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface ErrorResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
6
interfaces/settings.ts
Normal file
6
interfaces/settings.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export interface Settings {
|
||||
animations: boolean;
|
||||
ads: boolean;
|
||||
|
||||
rightSidebar: boolean;
|
||||
}
|
8
interfaces/spacial_data.ts
Normal file
8
interfaces/spacial_data.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export interface Point2D {
|
||||
x: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
export interface Point3D extends Point2D {
|
||||
y: number;
|
||||
}
|
30
interfaces/user.ts
Normal file
30
interfaces/user.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { BadgeNamed } from '@/utils/badges';
|
||||
import { PermissionNamed } from '@/utils/permissions';
|
||||
|
||||
export interface User {
|
||||
id: BigInt;
|
||||
|
||||
username: string;
|
||||
displayName: string;
|
||||
|
||||
email?: string;
|
||||
|
||||
avatar?: string;
|
||||
banner?: string;
|
||||
|
||||
accentColor?: number;
|
||||
|
||||
discordId?: BigInt;
|
||||
|
||||
subscription: number;
|
||||
|
||||
permissions: PermissionNamed[];
|
||||
badges: BadgeNamed[];
|
||||
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface TeamMember extends User {
|
||||
role: string;
|
||||
}
|
9
lib/ApiClient.ts
Normal file
9
lib/ApiClient.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
class ApiClient {
|
||||
private sessionToken: string;
|
||||
|
||||
constructor(sessionToken: string) {
|
||||
this.sessionToken = sessionToken;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiClient;
|
411
lib/Database.ts
411
lib/Database.ts
|
@ -1,58 +1,14 @@
|
|||
import { Role } from '@/utils/permissions';
|
||||
import { TeamMember, User } from '@/interfaces';
|
||||
import { getBadges } from '@/utils/badges';
|
||||
import { getPermissions } 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: Role;
|
||||
}
|
||||
import Discord from './Discord';
|
||||
import { createSessionToken, DiscordTokenData } from '@/utils/token_util';
|
||||
import snowflake from '@/utils/snowflake';
|
||||
|
||||
class Database {
|
||||
static instance: (Database | null) = null;
|
||||
|
||||
mysqlPool: (Pool | null) = null;
|
||||
private mysqlPool: (Pool | null) = null;
|
||||
|
||||
constructor() {
|
||||
if(Database.instance)
|
||||
|
@ -72,153 +28,252 @@ class Database {
|
|||
Database.instance = this;
|
||||
}
|
||||
|
||||
// Auth Stuff
|
||||
/**
|
||||
* Get a user from their id.
|
||||
*
|
||||
* @param id User's id
|
||||
*
|
||||
* @returns User
|
||||
*/
|
||||
async getUser(id: string): Promise<User | undefined> {
|
||||
const [ rows ] = await this.mysqlPool!.query('SELECT * FROM users WHERE id = ?', [ id ]);
|
||||
|
||||
async createUser(user: any): Promise<number> {
|
||||
const [ result ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE did = ?', [ user.id ]);
|
||||
if((rows as any[]).length === 0)
|
||||
return undefined;
|
||||
|
||||
if ((result as any).length > 0) {
|
||||
return (result as any)[0].id;
|
||||
}
|
||||
const row = (rows as any[])[0];
|
||||
|
||||
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 {
|
||||
id: row.id as BigInt,
|
||||
|
||||
return (res as any).insertId;
|
||||
username: row.username,
|
||||
displayName: row.display_name,
|
||||
|
||||
email: row.email,
|
||||
|
||||
avatar: row.avatar,
|
||||
banner: row.banner,
|
||||
|
||||
accentColor: row.accent_color,
|
||||
|
||||
discordId: row.discord_id as BigInt,
|
||||
|
||||
subscription: row.subscription,
|
||||
|
||||
permissions: getPermissions(row.permissions),
|
||||
badges: getBadges(row.badges),
|
||||
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
} as User;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a session for the user.
|
||||
* Get a user from their username.
|
||||
*
|
||||
* Should only be called by backend!
|
||||
* @param username User's username
|
||||
*
|
||||
* @param userData The data returned from the `/oauth2/token` endpoint.
|
||||
* @param userAgent The user agent of the user.
|
||||
*
|
||||
* @returns `string` The session token.
|
||||
* @returns User
|
||||
*/
|
||||
async createSession(userData: DiscordTokenData, userAgent?: string): Promise<string | null> {
|
||||
const res = await fetch(`${process.env.DISCORD_API}/users/@me`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${userData.access_token}`,
|
||||
},
|
||||
});
|
||||
async getUserUsername(username: string): Promise<User | undefined> {
|
||||
const [ rows ] = await this.mysqlPool!.query('SELECT * FROM users WHERE username = ?', [ username ]);
|
||||
|
||||
if (res.ok) {
|
||||
const user = await res.json();
|
||||
const uid = await this.createUser(user);
|
||||
if((rows as any[]).length === 0)
|
||||
return undefined;
|
||||
|
||||
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('=', '');
|
||||
const row = (rows as any[])[0];
|
||||
|
||||
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<void> {
|
||||
await this.mysqlPool!.execute('DELETE FROM sessions WHERE sid = ?', [ sid ]);
|
||||
}
|
||||
|
||||
async getSession(sid: string): Promise<any> {
|
||||
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<UserTable> {
|
||||
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE id = ?', [ id ]);
|
||||
|
||||
return (rows as UserTable[])[0];
|
||||
}
|
||||
|
||||
async getUsers(): Promise<UserTableWithoutEmail[]> {
|
||||
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<UserTable> {
|
||||
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE username = ?', [ username ]);
|
||||
|
||||
return (rows as UserTable[])[0];
|
||||
}
|
||||
|
||||
async getUserSessions(uid: number): Promise<any> {
|
||||
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM sessions WHERE uid = ?', [ uid ]);
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
async getTeam(): Promise<TeamMember[]> {
|
||||
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,
|
||||
id: row.id as BigInt,
|
||||
|
||||
username: row.username,
|
||||
global_name: row.global_name,
|
||||
displayName: row.display_name,
|
||||
|
||||
email: row.email,
|
||||
|
||||
avatar: row.avatar,
|
||||
banner: row.banner,
|
||||
accent_color: row.accent_color,
|
||||
permissions: row.permissions,
|
||||
role: (row.role as Role),
|
||||
};
|
||||
|
||||
accentColor: row.accent_color,
|
||||
|
||||
discordId: row.discord_id as BigInt,
|
||||
|
||||
subscription: row.subscription,
|
||||
|
||||
permissions: getPermissions(row.permissions),
|
||||
badges: getBadges(row.badges),
|
||||
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
} as User;
|
||||
}
|
||||
|
||||
private async getUserDiscord(discordId: BigInt): Promise<User | undefined> {
|
||||
const [ rows ] = await this.mysqlPool!.query('SELECT * FROM users WHERE discord_id = ?', [ discordId ]);
|
||||
|
||||
if((rows as any[]).length === 0)
|
||||
return undefined;
|
||||
|
||||
const row = (rows as any[])[0];
|
||||
|
||||
return {
|
||||
id: row.id as BigInt,
|
||||
|
||||
username: row.username,
|
||||
displayName: row.display_name,
|
||||
|
||||
email: row.email,
|
||||
|
||||
avatar: row.avatar,
|
||||
banner: row.banner,
|
||||
|
||||
accentColor: row.accent_color,
|
||||
|
||||
discordId: row.discord_id as BigInt,
|
||||
|
||||
subscription: row.subscription,
|
||||
|
||||
permissions: getPermissions(row.permissions),
|
||||
badges: getBadges(row.badges),
|
||||
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
} as User;
|
||||
}
|
||||
|
||||
async newUser(user: any): Promise<User> {
|
||||
const userExists = await this.getUserDiscord(user.id);
|
||||
|
||||
if(userExists)
|
||||
return userExists;
|
||||
|
||||
const id = snowflake.getUniqueID();
|
||||
|
||||
await this.mysqlPool!.query('INSERT INTO users (id, username, display_name, email, avatar, banner, accent_color, discord_id, permissions, badges) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
||||
id.toString(),
|
||||
|
||||
user.username,
|
||||
user.global_name,
|
||||
user.email,
|
||||
user.avatar,
|
||||
user.banner,
|
||||
user.accent_color,
|
||||
user.id
|
||||
]);
|
||||
|
||||
return {
|
||||
id: id,
|
||||
|
||||
username: user.username,
|
||||
displayName: user.display_name,
|
||||
|
||||
email: user.email,
|
||||
|
||||
avatar: user.avatar,
|
||||
banner: user.banner,
|
||||
|
||||
accentColor: user.accent_color,
|
||||
|
||||
discordId: user.discord as BigInt,
|
||||
|
||||
subscription: user.subscription,
|
||||
|
||||
permissions: getPermissions(user.permissions),
|
||||
badges: getBadges(user.badges),
|
||||
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
} as User;
|
||||
}
|
||||
|
||||
// Session -------------
|
||||
|
||||
/**
|
||||
* Create a new session.
|
||||
*
|
||||
* @param oauthData The data returned from the `/oauth2/token` endpoint.
|
||||
* @param userAgent Request's User-Agent
|
||||
* @param ip Request's IP
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
async newSession(oauthData: DiscordTokenData, userAgent: string, ip: string): Promise<string | undefined> {
|
||||
const discord = new Discord(oauthData.access_token);
|
||||
|
||||
const dcUser = await discord.user();
|
||||
const token = await createSessionToken(oauthData);
|
||||
|
||||
if(!dcUser.ok || !token)
|
||||
return undefined;
|
||||
|
||||
const user = await this.newUser(dcUser.data);
|
||||
|
||||
if(!user)
|
||||
return undefined;
|
||||
|
||||
try {
|
||||
await this.mysqlPool!.query('INSERT INTO user_sessions (token, user_id, dc_access_token, dc_refresh_token, dc_id_token, user_agent, ip, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
|
||||
token,
|
||||
user.id,
|
||||
oauthData.access_token,
|
||||
oauthData.refresh_token,
|
||||
oauthData.id_token,
|
||||
userAgent,
|
||||
ip,
|
||||
new Date(Date.now() + (oauthData.expires_in * 1000))
|
||||
]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
async deleteSession(id: string): Promise<boolean> {
|
||||
const [ result ] = await this.mysqlPool!.query('DELETE FROM user_sessions WHERE id = ?', [ id ]);
|
||||
|
||||
return (result as any).affectedRows === 1;
|
||||
}
|
||||
|
||||
async getSession(token: string): Promise<any | undefined> {
|
||||
const [ rows ] = await this.mysqlPool!.query('SELECT * FROM user_sessions WHERE token = ?', [ token ]);
|
||||
|
||||
if((rows as any[]).length === 0)
|
||||
return undefined;
|
||||
|
||||
const row = (rows as any[])[0];
|
||||
|
||||
if(new Date(row.expires_at) < new Date())
|
||||
return undefined;
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
async getSessions(userId: BigInt): Promise<any[]> {
|
||||
const [ rows ] = await this.mysqlPool!.query('SELECT * FROM user_sessions WHERE user_id = ?', [ userId ]);
|
||||
|
||||
return rows as any[];
|
||||
}
|
||||
|
||||
// Meta ----------------
|
||||
|
||||
async getTeam(): Promise<(TeamMember | undefined)[]> {
|
||||
const [ rows ] = await this.mysqlPool!.query('SELECT * FROM team');
|
||||
|
||||
const usersPromises = (rows as any[]).map(row => this.getUser(row.user_id));
|
||||
const users = await Promise.all(usersPromises);
|
||||
|
||||
return users.map((user, i) => {
|
||||
if (!user) return undefined;
|
||||
|
||||
return {
|
||||
...user,
|
||||
role: (rows as any[])[i].role
|
||||
} as TeamMember;
|
||||
});
|
||||
}
|
||||
|
||||
async getNations(): Promise<any> {
|
||||
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM nations');
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
async getNationCode(code: string): Promise<any> {
|
||||
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM nations WHERE code = ?', [ code ]);
|
||||
|
||||
return (rows as any)[0];
|
||||
}
|
||||
|
||||
async getCompaies(nid: number): Promise<any> {
|
||||
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;
|
22
lib/Discord.ts
Normal file
22
lib/Discord.ts
Normal file
|
@ -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;
|
38
lib/Storage.ts
Normal file
38
lib/Storage.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { ObjectCannedACL, PutObjectCommand, PutObjectCommandOutput, S3Client } from '@aws-sdk/client-s3';
|
||||
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
class S3Storage {
|
||||
private client;
|
||||
|
||||
// change this if it's different for you :3
|
||||
private bucket: string = 'crss';
|
||||
|
||||
constructor() {
|
||||
this.client = new S3Client({
|
||||
forcePathStyle: false,
|
||||
|
||||
region: process.env.S3_REGION!,
|
||||
endpoint: process.env.S3_ENDPOINT!,
|
||||
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY!,
|
||||
secretAccessKey: process.env.S3_SECRET_KEY!
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async uploadFile(key: string, file: (string | Uint8Array | Buffer | Readable), contetnType: string): Promise<PutObjectCommandOutput> {
|
||||
const res = await this.client.send(new PutObjectCommand({
|
||||
Bucket: this.bucket,
|
||||
Key: key,
|
||||
Body: file,
|
||||
ContentType: contetnType,
|
||||
ACL: ObjectCannedACL.public_read
|
||||
}));
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export default S3Storage;
|
|
@ -9,6 +9,7 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.637.0",
|
||||
"@icons-pack/react-simple-icons": "^10.0.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
|
@ -18,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",
|
||||
|
|
|
@ -7,7 +7,7 @@ import Database from '@/lib/Database';
|
|||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Role } from '@/utils/permissions';
|
||||
import { TeamRole } from '@/utils/badges';
|
||||
|
||||
import styles from '@/styles/About.module.scss';
|
||||
import { Globe } from 'lucide-react';
|
||||
|
@ -47,8 +47,8 @@ export default function About({ teamMembers }: { teamMembers: any[] }) {
|
|||
|
||||
<h3>{member.global_name}</h3>
|
||||
|
||||
{member.role === Role.Owner && <label>Owner</label>}
|
||||
{member.role === Role.Admin && <label>Admin</label>}
|
||||
{member.role === TeamRole.Owner && <label>Owner</label>}
|
||||
{member.role === TeamRole.Admin && <label>Admin</label>}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
|
22
pages/api/index.ts
Normal file
22
pages/api/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<any>,
|
||||
) {
|
||||
res
|
||||
.status(200)
|
||||
.json({
|
||||
latest: 0,
|
||||
versions: [
|
||||
{
|
||||
version: {
|
||||
name: 'v1.0.0',
|
||||
code: 1,
|
||||
},
|
||||
path: '/v1',
|
||||
deprecated: false,
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
|
@ -1,31 +1,26 @@
|
|||
import Database from '@/lib/Database';
|
||||
import { serialize } from 'cookie';
|
||||
|
||||
import Discord from '@/lib/Discord';
|
||||
import S3Storage from '@/lib/Storage';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
type Data = {
|
||||
sid: string | null;
|
||||
};
|
||||
|
||||
type Error = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<Data | Error>,
|
||||
res: NextApiResponse<any>,
|
||||
) {
|
||||
const db = new Database();
|
||||
const s3 = new S3Storage();
|
||||
|
||||
const { code, state } = req.query;
|
||||
|
||||
console.log(code, state);
|
||||
|
||||
const discordApi = process.env.DISCORD_API!;
|
||||
if (typeof code !== 'string' || typeof state !== 'string') {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: 'Invalid Request'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof code === 'string') {
|
||||
const data = await fetch(`${discordApi}/oauth2/token`, {
|
||||
const data = await fetch(`${process.env.DISCORD_API}/oauth2/token`, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
client_id: process.env.DISCORD_CLIENT!,
|
||||
|
@ -37,35 +32,47 @@ export default async function handler(
|
|||
});
|
||||
|
||||
const json = await data.json();
|
||||
const sid = await db.createSession(json, req.headers['user-agent']);
|
||||
const token = await db.newSession(json, req.headers['user-agent']!, (req.headers['x-forwarded-for'] as string || req.socket.remoteAddress!));
|
||||
|
||||
if (sid) {
|
||||
const cookie = serialize('session', sid, {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
path: '/',
|
||||
expires: new Date(Date.now() + json.expires_in * 1000),
|
||||
if (!token) {
|
||||
return res.status(500).json({
|
||||
code: 500,
|
||||
message: 'Internal Server Error'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
token
|
||||
});
|
||||
|
||||
res.setHeader('Set-Cookie', cookie);
|
||||
try {
|
||||
const dc = new Discord(json.access_token);
|
||||
const dcUser = await dc.user();
|
||||
|
||||
if ((state as string).startsWith('/'))
|
||||
res.status(302).redirect(state as string);
|
||||
else
|
||||
res.status(400).json({ error: 'Invalid redirect uri in state!' });
|
||||
const user = await db.getUser((await db.getSession(token)).user_id);
|
||||
|
||||
if (dcUser.data.avatar) {
|
||||
const fetchData = await fetch(`https://cdn.discordapp.com/avatars/${dcUser.data.id}/${dcUser.data.avatar}.png?size=1024`);
|
||||
const buffer = await fetchData.arrayBuffer();
|
||||
|
||||
await s3.uploadFile(`users/${user?.id}/avatar/${dcUser.data.avatar}.png`, Buffer.from(buffer), 'image/png');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (dcUser.data.banner) {
|
||||
const fetchData = await fetch(`https://cdn.discordapp.com/banners/${dcUser.data.id}/${dcUser.data.banner}.png?size=1024`);
|
||||
const buffer = await fetchData.arrayBuffer();
|
||||
|
||||
res.status(500).json(
|
||||
{ error: 'Internal Server Error' }
|
||||
);
|
||||
await s3.uploadFile(`users/${user?.id}/banner/${dcUser.data.banner}.png`, Buffer.from(buffer), 'image/png');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
res.status(400).json(
|
||||
{ error: 'Invalid code' }
|
||||
);
|
||||
return res.status(500).json({
|
||||
code: 500,
|
||||
message: 'Internal Server Error'
|
||||
});
|
||||
}
|
||||
}
|
20
pages/api/v1/meta/index.ts
Normal file
20
pages/api/v1/meta/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import getConfig from 'next/config';
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<any>,
|
||||
) {
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
let { git } = publicRuntimeConfig;
|
||||
|
||||
git.commit.created = new Date(git.commit.created).getTime();
|
||||
|
||||
res
|
||||
.status(200)
|
||||
.json({
|
||||
git,
|
||||
});
|
||||
}
|
36
pages/api/v1/meta/team.ts
Normal file
36
pages/api/v1/meta/team.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { ErrorResponse, TeamMember, User } from '@/interfaces';
|
||||
|
||||
import Database from '@/lib/Database';
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<TeamMember[] | ErrorResponse>,
|
||||
) {
|
||||
const db = new Database();
|
||||
|
||||
try {
|
||||
let team: any[] = await db.getTeam();
|
||||
|
||||
team = team.map((member: TeamMember) => {
|
||||
return {
|
||||
...member,
|
||||
email: undefined,
|
||||
discordId: undefined,
|
||||
|
||||
createdAt: new Date(member.createdAt).getTime(),
|
||||
updatedAt: new Date(member.updatedAt).getTime()
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).json(team);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: 'Internal Server Error'
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
type Error = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
interface ServerInfo {
|
||||
version: string;
|
||||
online: number;
|
||||
worlds: string[];
|
||||
}
|
||||
|
||||
import * as net from 'node:net';
|
||||
|
||||
export default function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<ServerInfo | Error>,
|
||||
) {
|
||||
try {
|
||||
const mc_api = process.env.MC_API!.split(':');
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: mc_api[0],
|
||||
port: parseInt(mc_api[1]),
|
||||
}, async () => {
|
||||
const reqData = Buffer.alloc(1 + 4);
|
||||
|
||||
reqData.writeInt8(0x00, 0);
|
||||
reqData.writeUint32BE(0, 1);
|
||||
|
||||
socket.write(reqData);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const packetId = data[0];
|
||||
const length = data.readUInt32BE(1); // unused but in case someone wants to verify the lenght :3
|
||||
|
||||
if (packetId !== 0x00) {
|
||||
socket.end();
|
||||
|
||||
return res.status(500).json({ message: 'There was an error with the server.' });
|
||||
}
|
||||
|
||||
const jsonData = data.toString('utf-8', 5);
|
||||
|
||||
socket.end();
|
||||
|
||||
res.status(200).json(JSON.parse(jsonData));
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error(err);
|
||||
|
||||
socket.end();
|
||||
|
||||
res.status(500).json({ message: 'There was an error with the server.' });
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ message: 'There was an error with the server.' });
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import Database from '@/lib/Database';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { serialize } from 'cookie';
|
||||
|
||||
type Data = {
|
||||
success: string;
|
||||
};
|
||||
|
||||
interface Error {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<Data | Error>,
|
||||
) {
|
||||
const db = new Database();
|
||||
const sid = req.cookies.session;
|
||||
|
||||
if (!sid)
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
const session = await db.getSession(sid!);
|
||||
|
||||
if (!session)
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
const user = await db.getUser(session.uid);
|
||||
|
||||
if (!user)
|
||||
return res.status(404).json({ error: 'Not Found' });
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
db.deleteSession(sid!);
|
||||
|
||||
res.setHeader('Set-Cookie', serialize('session', '', {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
path: '/',
|
||||
expires: new Date(0),
|
||||
}));
|
||||
|
||||
return res.status(200).json({ success: 'Delete Session for ' + user.username });
|
||||
}
|
||||
|
||||
res.status(405).json({ error: 'Method Not Allowed' });
|
||||
}
|
|
@ -1,52 +1,46 @@
|
|||
import { ErrorResponse, User } from '@/interfaces';
|
||||
import Database from '@/lib/Database';
|
||||
import { getAuthenticatedUser } from '@/utils/auth_util';
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
type Data = {
|
||||
id: number;
|
||||
discord_id: string;
|
||||
names: {
|
||||
username: string;
|
||||
global_name: string;
|
||||
};
|
||||
email: string;
|
||||
avatar?: string;
|
||||
banner?: string;
|
||||
accent_color?: number;
|
||||
permissions: number;
|
||||
};
|
||||
|
||||
interface Error {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<Data | Error>,
|
||||
res: NextApiResponse<User | ErrorResponse>,
|
||||
) {
|
||||
const db = new Database();
|
||||
const sid = req.cookies.session;
|
||||
const user = await getAuthenticatedUser(req);
|
||||
|
||||
if (!sid)
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
if (!user)
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: 'Unauthorized'
|
||||
});
|
||||
|
||||
const session = await db.getSession(sid!);
|
||||
if (req.method === 'GET') {
|
||||
return res.status(200).json(user);
|
||||
}
|
||||
|
||||
if (!session)
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
if (req.method === 'PATCH') {
|
||||
// TODO: implement
|
||||
|
||||
const user = await db.getUser(session.uid);
|
||||
return res.status(501).json({
|
||||
code: 501,
|
||||
message: 'Not Yet Implemented'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
id: user.id,
|
||||
discord_id: user.did,
|
||||
names: {
|
||||
username: user.username,
|
||||
global_name: user.global_name
|
||||
},
|
||||
email: user.email,
|
||||
avatar: user.avatar,
|
||||
banner: user.banner,
|
||||
accent_color: user.accent_color,
|
||||
permissions: user.permissions
|
||||
if (req.method === 'DELETE') {
|
||||
// TODO: implement
|
||||
|
||||
return res.status(501).json({
|
||||
code: 501,
|
||||
message: 'Not Yet Implemented'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(405).json({
|
||||
code: 405,
|
||||
message: 'Method Not Allowed'
|
||||
});
|
||||
}
|
38
pages/api/v1/user/@me/session/[id].ts
Normal file
38
pages/api/v1/user/@me/session/[id].ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { ErrorResponse, User } from '@/interfaces';
|
||||
import Database from '@/lib/Database';
|
||||
import { getAuthenticatedUser } from '@/utils/auth_util';
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<any>, // User | ErrorResponse
|
||||
) {
|
||||
const db = new Database();
|
||||
const user = await getAuthenticatedUser(req);
|
||||
|
||||
const { id } = req.query;
|
||||
|
||||
if (!user)
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: 'Unauthorized'
|
||||
});
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
const didDelete = await db.deleteSession(id as string);
|
||||
|
||||
if (!didDelete)
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: 'Session Not Found'
|
||||
});
|
||||
|
||||
return res.status(204).end();
|
||||
}
|
||||
|
||||
return res.status(405).json({
|
||||
code: 405,
|
||||
message: 'Method Not Allowed'
|
||||
});
|
||||
}
|
40
pages/api/v1/user/@me/sessions.ts
Normal file
40
pages/api/v1/user/@me/sessions.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { ErrorResponse, User } from '@/interfaces';
|
||||
import Database from '@/lib/Database';
|
||||
import { getAuthenticatedUser } from '@/utils/auth_util';
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<any>, // User | ErrorResponse
|
||||
) {
|
||||
const db = new Database();
|
||||
const user = await getAuthenticatedUser(req);
|
||||
|
||||
if (!user)
|
||||
return res.status(401).json({
|
||||
code: 401,
|
||||
message: 'Unauthorized'
|
||||
});
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const sessions = await db.getSessions(user.id);
|
||||
|
||||
return res.status(200).json(
|
||||
sessions.map((session) => ({
|
||||
id: session.id,
|
||||
|
||||
ip: session.ip,
|
||||
userAgent: session.user_agent,
|
||||
|
||||
createdAt: session.created_at,
|
||||
expiresAt: session.expires_at,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(405).json({
|
||||
code: 405,
|
||||
message: 'Method Not Allowed'
|
||||
});
|
||||
}
|
71
pages/api/v1/user/[id].ts
Normal file
71
pages/api/v1/user/[id].ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { ErrorResponse, User } from '@/interfaces';
|
||||
|
||||
import Database from '@/lib/Database';
|
||||
import { getAuthenticatedUser, reqHasValidToken } from '@/utils/auth_util';
|
||||
import { getPermission, hasPermission, Permission, PermissionNamed } from '@/utils/permissions';
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<User | ErrorResponse>,
|
||||
) {
|
||||
const db = new Database();
|
||||
|
||||
const { id } = req.query;
|
||||
|
||||
let shouldShowSensitive = false;
|
||||
|
||||
// tf was I on?
|
||||
// const valid = await reqHasValidToken(req);
|
||||
|
||||
// thats better
|
||||
const vUser = await getAuthenticatedUser(req);
|
||||
|
||||
if (!vUser)
|
||||
shouldShowSensitive = false;
|
||||
|
||||
if (
|
||||
hasPermission(getPermission(vUser!.permissions), Permission.SuperAdmin) ||
|
||||
vUser!.id === BigInt(id as string)
|
||||
)
|
||||
shouldShowSensitive = true;
|
||||
|
||||
if ((/^\d+$/).test(id as string)) {
|
||||
let user = await db.getUser((id as string));
|
||||
|
||||
if (!user)
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: 'User Not Found'
|
||||
});
|
||||
|
||||
user = {
|
||||
...user,
|
||||
|
||||
email: shouldShowSensitive ? user.email : undefined,
|
||||
discordId: shouldShowSensitive ? user.discordId : undefined,
|
||||
};
|
||||
|
||||
res.status(200).json(user);
|
||||
} else {
|
||||
let user: any = await db.getUserUsername(id as string);
|
||||
|
||||
if (!user)
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
message: 'User Not Found'
|
||||
});
|
||||
|
||||
|
||||
// TODO: check if user is admin or itself and show email and discordId
|
||||
user = {
|
||||
...user,
|
||||
|
||||
email: shouldShowSensitive ? user.email : undefined,
|
||||
discordId: shouldShowSensitive ? user.discordId : undefined,
|
||||
};
|
||||
|
||||
res.status(200).json(user);
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
import Database from '@/lib/Database';
|
||||
import { isUserAdmin } from '@/utils/auth_util';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
type Data = {
|
||||
id: number;
|
||||
discord_id: string;
|
||||
names: {
|
||||
username: string;
|
||||
global_name: string;
|
||||
};
|
||||
avatar?: string;
|
||||
banner?: string;
|
||||
accent_color?: number;
|
||||
permissions: number;
|
||||
};
|
||||
|
||||
interface Error {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<Data | Error>,
|
||||
) {
|
||||
const db = new Database();
|
||||
|
||||
const user = await db.getUserUsername(req.query.username as string);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'Not Found' });
|
||||
}
|
||||
|
||||
// hehe only admins update users :trolley:
|
||||
// also validation yeah uh... didn't have budget for that
|
||||
// tech debt for the win
|
||||
if (req.method === 'PATCH') {
|
||||
const sid = req.cookies.session;
|
||||
|
||||
const isAdmin = isUserAdmin(sid);
|
||||
|
||||
if (!isAdmin) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { permissions } = req.body;
|
||||
|
||||
if (permissions) {
|
||||
await db.updateUserPermissions(user.id, permissions);
|
||||
}
|
||||
|
||||
res.status(204).end();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
id: user.id,
|
||||
discord_id: user.did,
|
||||
names: {
|
||||
username: user.username,
|
||||
global_name: user.global_name
|
||||
},
|
||||
avatar: user.avatar,
|
||||
banner: user.banner,
|
||||
accent_color: user.accent_color,
|
||||
permissions: user.permissions
|
||||
});
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import Database from '@/lib/Database';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
interface Response {
|
||||
id: number;
|
||||
did: string;
|
||||
username: string;
|
||||
global_name: string;
|
||||
avatar?: string;
|
||||
banner?: string;
|
||||
accent_color?: number;
|
||||
permissions: number;
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<Response[]>,
|
||||
) {
|
||||
const db = new Database();
|
||||
|
||||
const users = await db.getUsers();
|
||||
|
||||
res.status(200).json(users);
|
||||
}
|
1224
pnpm-lock.yaml
1224
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,8 @@
|
|||
import Database from '@/lib/Database';
|
||||
import { hasPermission, Permission } from './permissions';
|
||||
import { getPermission, hasPermission, Permission } from './permissions';
|
||||
import { User } from '@/interfaces';
|
||||
|
||||
import type { NextApiRequest } from 'next';
|
||||
|
||||
const db = new Database();
|
||||
|
||||
|
@ -24,6 +27,49 @@ export async function isUserAdmin(sid?: string): Promise<doas> {
|
|||
|
||||
return {
|
||||
user,
|
||||
hasPermission: hasPermission(user.permissions, Permission.Admin)
|
||||
hasPermission: hasPermission(getPermission(user.permissions), Permission.Admin)
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAuthenticatedUser(req: NextApiRequest): Promise<User | undefined> {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
const token = authorization?.split(' ')!;
|
||||
|
||||
if (!token)
|
||||
return undefined;
|
||||
|
||||
if (token[0] !== 'Bearer' || !token[1])
|
||||
return undefined;
|
||||
|
||||
const session = await db.getSession(token[1]);
|
||||
|
||||
if (!session)
|
||||
return undefined;
|
||||
|
||||
const user = await db.getUser(session.user_id);
|
||||
|
||||
if (!user)
|
||||
return undefined;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function reqHasValidToken(req: NextApiRequest): Promise<boolean> {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
const token = authorization?.split(' ')!;
|
||||
|
||||
if (!token)
|
||||
return false;
|
||||
|
||||
if (token[0] !== 'Bearer' || !token[1])
|
||||
return false;
|
||||
|
||||
const session = await db.getSession(token[1]);
|
||||
|
||||
if (!session)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
29
utils/badges.ts
Normal file
29
utils/badges.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
export enum TeamRole {
|
||||
Owner = 'owner',
|
||||
Admin = 'admin',
|
||||
Moderator = 'mod'
|
||||
}
|
||||
|
||||
export enum Badge {
|
||||
Old = 1 << 0, // CRSS OG
|
||||
Supporter = 1 << 1, // "Donator"
|
||||
}
|
||||
|
||||
export enum BadgeNamed {
|
||||
Old = 'og',
|
||||
Supporter = 'supporter',
|
||||
}
|
||||
|
||||
export function getBadges(badges: number): BadgeNamed[] {
|
||||
const result: BadgeNamed[] = [];
|
||||
|
||||
if ((badges & Badge.Old) === Badge.Old) {
|
||||
result.push(BadgeNamed.Old);
|
||||
}
|
||||
|
||||
if ((badges & Badge.Supporter) === Badge.Supporter) {
|
||||
result.push(BadgeNamed.Supporter);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
|
@ -1,12 +1,56 @@
|
|||
export enum Permission {
|
||||
Admin = 1 << 0
|
||||
SuperAdmin = 1 << 0,
|
||||
Admin = 1 << 1,
|
||||
ServerPlayer = 1 << 2
|
||||
}
|
||||
|
||||
export enum Role {
|
||||
Owner = 'owner',
|
||||
export enum PermissionNamed {
|
||||
SuperAdmin = 'super_admin',
|
||||
Admin = 'admin',
|
||||
ServerPlayer = 'server_player'
|
||||
}
|
||||
|
||||
export function getPermission(permissions: PermissionNamed[]): Permission {
|
||||
let result = 0;
|
||||
|
||||
for (const permission of permissions) {
|
||||
switch (permission) {
|
||||
case PermissionNamed.SuperAdmin:
|
||||
result |= Permission.SuperAdmin;
|
||||
|
||||
break;
|
||||
case PermissionNamed.Admin:
|
||||
result |= Permission.Admin;
|
||||
|
||||
break;
|
||||
case PermissionNamed.ServerPlayer:
|
||||
result |= Permission.ServerPlayer;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function hasPermission(permissions: number, permission: Permission): boolean {
|
||||
return (permissions & permission) === permission;
|
||||
}
|
||||
|
||||
export function getPermissions(permissions: number): PermissionNamed[] {
|
||||
const result: PermissionNamed[] = [];
|
||||
|
||||
if ((permissions & Permission.SuperAdmin) === Permission.SuperAdmin) {
|
||||
result.push(PermissionNamed.SuperAdmin);
|
||||
}
|
||||
|
||||
if ((permissions & Permission.Admin) === Permission.Admin) {
|
||||
result.push(PermissionNamed.Admin);
|
||||
}
|
||||
|
||||
if ((permissions & Permission.ServerPlayer) === Permission.ServerPlayer) {
|
||||
result.push(PermissionNamed.ServerPlayer);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
8
utils/snowflake.ts
Normal file
8
utils/snowflake.ts
Normal file
|
@ -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;
|
52
utils/token_util.ts
Normal file
52
utils/token_util.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import crypto from 'node:crypto';
|
||||
|
||||
import snowflake from './snowflake';
|
||||
|
||||
import Discord from '@/lib/Discord';
|
||||
|
||||
export 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<string | undefined> {
|
||||
const discord = new Discord(oauthData.access_token);
|
||||
const res = await discord.user();
|
||||
|
||||
if (!res.ok)
|
||||
return undefined;
|
||||
|
||||
const dcUser = res.data;
|
||||
|
||||
const hmac = crypto
|
||||
.createHmac('sha256', process.env.AUTH_SECRET!)
|
||||
.update(oauthData.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}`;
|
||||
}
|
Loading…
Reference in a new issue