Compare commits

..

20 commits

Author SHA1 Message Date
TheClashFruit a4173e24ba
fix: fix authorization in user/:id
All checks were successful
Lint Codebase / lint (push) Successful in 1m3s
2024-09-07 16:56:09 +02:00
TheClashFruit d9377a9731
feat: delete sessions 2024-09-07 16:48:06 +02:00
TheClashFruit e4012844a6
feat: sessions endpoint
All checks were successful
Lint Codebase / lint (push) Successful in 1m12s
2024-09-07 16:42:52 +02:00
TheClashFruit a9f943b7e4
fix: auth util fixes 2024-09-07 16:38:00 +02:00
TheClashFruit 89a31da575
feat: authorization stuff 2024-09-07 16:33:25 +02:00
TheClashFruit c6881a16da
feat: some user apis and storage stuff
All checks were successful
Lint Codebase / lint (push) Successful in 1m24s
2024-09-07 16:08:27 +02:00
TheClashFruit 4e7c2a1df2
feat: authentication
All checks were successful
Lint Codebase / lint (push) Successful in 56s
2024-09-01 16:45:58 +02:00
TheClashFruit 0a8b61c472
feat: add /meta apis
All checks were successful
Lint Codebase / lint (push) Successful in 54s
2024-09-01 15:47:31 +02:00
TheClashFruit bd27f0084c
feat: the old api mess be gone 2024-09-01 15:10:54 +02:00
TheClashFruit 36ac57378a
feat: prepare db stuff for the new api
All checks were successful
Lint Codebase / lint (push) Successful in 56s
2024-09-01 14:37:26 +02:00
TheClashFruit 57e5f69bc8
feat: add db structure
All checks were successful
Lint Codebase / lint (push) Successful in 55s
2024-09-01 14:07:55 +02:00
TheClashFruit 6c8793ff68
feat: remove parliamentBulding from Government 2024-09-01 13:52:22 +02:00
TheClashFruit 54cf25a554
feat: add settings type 2024-09-01 13:11:31 +02:00
TheClashFruit 1dde9b1a84
feat: add updated and created dates, add discord id to user 2024-09-01 13:00:21 +02:00
TheClashFruit d6db115c08
fix: use Permission and Badge for permissions and badges
All checks were successful
Lint Codebase / lint (push) Successful in 56s
2024-09-01 12:38:02 +02:00
TheClashFruit dbbefde41e
feat: internal types
All checks were successful
Lint Codebase / lint (push) Successful in 59s
2024-09-01 12:34:33 +02:00
TheClashFruit ada5cdd47c
feat: setup s3 stuff 2024-09-01 12:07:38 +02:00
TheClashFruit a5aae6e9df
feat(ci): require lint to complete before deploying
All checks were successful
Lint Codebase / lint (push) Successful in 1m7s
2024-09-01 11:49:31 +02:00
TheClashFruit 638452eb0b
feat: badges 2024-09-01 11:38:46 +02:00
TheClashFruit 9d6e607ab5
feat: preperations for the api 2024-09-01 11:30:26 +02:00
47 changed files with 5779 additions and 3518 deletions

View file

@ -11,5 +11,12 @@ DB_HOST=
DB_PORT=
AUTH_SECRET=
LINK_SECRET=
MC_API=
MC_API=
S3_REGION="fra1"
S3_ENDPOINT="https://fra1.digitaloceanspaces.com"
S3_ACCESS_KEY=
S3_SECRET_KEY=

View file

@ -3,7 +3,8 @@
"rules": {
"indent": [
"error",
2
2,
{ "SwitchCase": 1 }
],
"quotes": [
"error",

View file

@ -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

View file

@ -1,5 +0,0 @@
{
"tabWidth": 2,
"singleQuote": true,
"semi": true
}

15
.vscode/launch.json vendored
View file

@ -1,15 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}

View file

@ -1,6 +0,0 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View file

@ -1,7 +1,6 @@
import {
Menu,
Home,
Scale,
AtSign,
Images,
Map,
@ -12,9 +11,7 @@ import {
LogOut,
LogIn,
Earth,
X,
PlusIcon,
Plus,
X
} from 'lucide-react';
import Link from 'next/link';
@ -31,19 +28,22 @@ import { useEffect, useRef, useState } from 'react';
import Dropdown from './Dropdown';
import { useUser } from '@/context/UserContext';
import { Permission, hasPermission } from '@/utils/permissions';
import {
Permission,
hasPermission
} from '@/utils/permissions';
export default function NavBar({ currentPage }: { currentPage: string }) {
const { user, isLoggedIn } = useUser();
const { user, isLoggedIn } = useUser();
const [navOpen, setNavOpen] = useState(false);
const [ navOpen, setNavOpen ] = useState(false);
const { publicRuntimeConfig } = getConfig();
const router = useRouter();
const server = {
version: '1.16.5',
version: '1.15.2'
};
const buildDiscordUrl = (): string => {
@ -51,14 +51,8 @@ export default function NavBar({ currentPage }: { currentPage: string }) {
url.searchParams.append('client_id', publicRuntimeConfig.discord.clientId);
url.searchParams.append('response_type', 'code');
url.searchParams.append(
'redirect_uri',
publicRuntimeConfig.discord.redirectUri
);
url.searchParams.append(
'scope',
publicRuntimeConfig.discord.scopes.join(' ')
);
url.searchParams.append('redirect_uri', publicRuntimeConfig.discord.redirectUri);
url.searchParams.append('scope', publicRuntimeConfig.discord.scopes.join(' '));
url.searchParams.append('state', router.asPath);
@ -77,17 +71,15 @@ export default function NavBar({ currentPage }: { currentPage: string }) {
</div>
<div>
<label htmlFor="ip">Server Address:</label>
<label htmlFor="ip">
Server Address:
</label>
<input
type="text"
value="play.crss.cc"
id="ip"
readOnly
size={8}
/>
<input type="text" value="play.crss.cc" id="ip" readOnly size={8} />
<label htmlFor="ip">Version: {server.version}</label>
<label htmlFor="ip">
Version: {server.version}
</label>
</div>
</div>
</div>
@ -96,170 +88,132 @@ export default function NavBar({ currentPage }: { currentPage: string }) {
<nav className={`${styles.navBar} ${navOpen ? styles.navOpen : ''}`}>
<div className={styles.container}>
<div className={styles.navMobileContainer}>
<button
className={styles.navToggle}
onClick={() => {
setNavOpen(!navOpen);
}}
>
{!navOpen ? <Menu /> : <X />}
<button className={styles.navToggle} onClick={() => { setNavOpen(!navOpen); }}>
{ !navOpen ? <Menu /> : <X /> }
</button>
</div>
<div className={styles.navCollapse}>
<ul>
<li>
<Link
href={currentPage == 'home' ? '#' : '/'}
className={currentPage == 'home' ? styles.active : ''}
>
<Link href={currentPage == 'home' ? '#' : '/'} className={currentPage == 'home' ? styles.active : ''}>
<Home />
Home
</Link>
</li>
<li>
<Link
href={currentPage == 'rules' ? '#' : '/rules'}
className={currentPage == 'rules' ? styles.active : ''}
>
<Scale />
Rules
</Link>
</li>
<li>
<Link
href={currentPage == 'about' ? '#' : '/about'}
className={currentPage == 'about' ? styles.active : ''}
>
<Link href={currentPage == 'about' ? '#' : '/about'} className={currentPage == 'about' ? styles.active : ''}>
<AtSign />
About
</Link>
</li>
<li>
<Link
href={currentPage == 'gallery' ? '#' : '/gallery'}
className={currentPage == 'gallery' ? styles.active : ''}
>
<Link href={currentPage == 'gallery' ? '#' : '/gallery'} className={currentPage == 'gallery' ? styles.active : ''}>
<Images />
Gallery
</Link>
</li>
<li>
<Link
href={currentPage == 'map' ? '#' : '/map'}
className={currentPage == 'map' ? styles.active : ''}
>
<Link href={currentPage == 'map' ? '#' : '/map'} className={currentPage == 'map' ? styles.active : ''}>
<Map />
Map
</Link>
</li>
<li>
<Link
href={currentPage == 'nations' ? '#' : '/nations'}
className={currentPage == 'nations' ? styles.active : ''}
>
<Link href={currentPage == 'nations' ? '#' : '/nations'} className={currentPage == 'nations' ? styles.active : ''}>
<Earth />
Nations
</Link>
</li>
</ul>
<ul>
{isLoggedIn && user && (
{(isLoggedIn && user) && (
<li>
{(hasPermission(user.permissions, Permission.Admin) && (
<Dropdown
items={[
{
icon: User,
label: 'Profile',
href: `/u/${
user ? user.names.username : 'Loading...'
}`,
},
{
icon: Settings,
label: 'Settings',
href: '/settings',
},
{
icon: Plus,
label: 'Premium+',
href: '/pp',
},
{
divider: true,
},
{
icon: LayoutDashboard,
label: 'Admin',
href: '/admin',
},
{
icon: LogOut,
label: 'Logout',
onClick: async (e) => {
e.preventDefault();
{hasPermission(user.permissions, Permission.Admin) && (
<Dropdown items={[
{
icon: User,
label: 'Profile',
href: `/u/${user ? user.names.username : 'Loading...'}`
},
{
icon: Settings,
label: 'Settings',
href: '/settings'
},
{
divider: true
},
{
icon: LayoutDashboard,
label: 'Admin',
href: '/admin'
},
{
icon: LogOut,
label: 'Logout',
onClick: async (e) => {
e.preventDefault();
await fetch('/api/v1/session', {
method: 'DELETE',
});
await fetch('/api/v1/session', {
method: 'DELETE'
});
router.reload();
},
},
]}
className={styles.dropDown}
>
router.reload();
}
}
]} className={styles.dropDown}>
<User />
{user ? user.names.global_name : 'Loading...'}
</Dropdown>
)) || (
<Dropdown
items={[
{
icon: User,
label: 'Profile',
href: `/u/${
user ? user.names.username : 'Loading...'
}`,
},
{
icon: Settings,
label: 'Settings',
href: '/settings',
},
{
divider: true,
},
{
icon: LogOut,
label: 'Logout',
onClick: async (e) => {
e.preventDefault();
) || (
<Dropdown items={[
{
icon: User,
label: 'Profile',
href: `/u/${user ? user.names.username : 'Loading...'}`
},
{
icon: Settings,
label: 'Settings',
href: '/settings'
},
{
divider: true
},
{
icon: LogOut,
label: 'Logout',
onClick: async (e) => {
e.preventDefault();
await fetch('/api/v1/session', {
method: 'DELETE',
});
await fetch('/api/v1/session', {
method: 'DELETE'
});
router.reload();
},
},
]}
className={styles.dropDown}
>
router.reload();
}
}
]} className={styles.dropDown}>
<User />
{user ? user.names.global_name : 'Loading...'}
</Dropdown>
)}
</li>
)}
{!isLoggedIn && !user && (
{(!isLoggedIn && !user) && (
<li>
<Link href={buildDiscordUrl()}>
<LogIn />
Login
</Link>
</li>

25
docs/cdn.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,4 @@
export interface ErrorResponse {
code: number;
message: string;
}

6
interfaces/settings.ts Normal file
View file

@ -0,0 +1,6 @@
export interface Settings {
animations: boolean;
ads: boolean;
rightSidebar: boolean;
}

View 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
View 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
View file

@ -0,0 +1,9 @@
class ApiClient {
private sessionToken: string;
constructor(sessionToken: string) {
this.sessionToken = sessionToken;
}
}
export default ApiClient;

View file

@ -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;
static instance: (Database | 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);
return {
id: row.id as BigInt,
const sid = base + '.' + date + '.' + sum.digest('hex');
username: row.username,
displayName: row.display_name,
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,
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;
}
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,
new Date(Date.now() + (userData.expires_in * 1000)),
ip,
new Date(Date.now() + (oauthData.expires_in * 1000))
]);
} catch (e) {
console.error(e);
return sid;
} else {
throw new Error('Error Fetching Discord User Data');
return undefined;
}
return token;
}
async deleteSession(sid: string): Promise<void> {
await this.mysqlPool!.execute('DELETE FROM sessions WHERE sid = ?', [ sid ]);
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(sid: string): Promise<any> {
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM sessions WHERE sid = ?', [ sid ]);
async getSession(token: string): Promise<any | undefined> {
const [ rows ] = await this.mysqlPool!.query('SELECT * FROM user_sessions WHERE token = ?', [ token ]);
return (rows as any)[0];
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;
}
// End of Auth Stuff
async getSessions(userId: BigInt): Promise<any[]> {
const [ rows ] = await this.mysqlPool!.query('SELECT * FROM user_sessions WHERE user_id = ?', [ userId ]);
async getUser(id: number): Promise<UserTable> {
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE id = ?', [ id ]);
return (rows as UserTable[])[0];
return rows as any[];
}
async getUsers(): Promise<UserTableWithoutEmail[]> {
const [ rows ] = await this.mysqlPool!.execute('SELECT id, did, username, global_name, avatar, banner, accent_color, permissions FROM users');
// Meta ----------------
return rows as UserTableWithoutEmail[];
}
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);
async getUserUsername(username: string): Promise<UserTable> {
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE username = ?', [ username ]);
return users.map((user, i) => {
if (!user) return undefined;
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,
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 Role),
};
...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
View 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
View 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;

View file

@ -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",

View file

@ -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
View 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,
}
]
});
}

View file

@ -1,71 +1,78 @@
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!;
try {
if (typeof code === 'string') {
const data = await fetch(`${discordApi}/oauth2/token`, {
method: 'POST',
body: new URLSearchParams({
client_id: process.env.DISCORD_CLIENT!,
client_secret: process.env.DISCORD_SECRET!,
grant_type: 'authorization_code',
code,
redirect_uri: process.env.DISCORD_REDIRECT!,
}),
});
const json = await data.json();
const sid = await db.createSession(json, req.headers['user-agent']);
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),
});
res.setHeader('Set-Cookie', cookie);
if ((state as string).startsWith('/'))
res.status(302).redirect(state as string);
else
res.status(400).json({ error: 'Invalid redirect uri in state!' });
}
return;
}
} catch (error) {
console.error(error);
res.status(500).json(
{ error: 'Internal Server Error' }
);
if (typeof code !== 'string' || typeof state !== 'string') {
return res.status(400).json({
code: 400,
message: 'Invalid Request'
});
}
res.status(400).json(
{ error: 'Invalid code' }
);
try {
const data = await fetch(`${process.env.DISCORD_API}/oauth2/token`, {
method: 'POST',
body: new URLSearchParams({
client_id: process.env.DISCORD_CLIENT!,
client_secret: process.env.DISCORD_SECRET!,
grant_type: 'authorization_code',
code,
redirect_uri: process.env.DISCORD_REDIRECT!,
}),
});
const json = await data.json();
const token = await db.newSession(json, req.headers['user-agent']!, (req.headers['x-forwarded-for'] as string || req.socket.remoteAddress!));
if (!token) {
return res.status(500).json({
code: 500,
message: 'Internal Server Error'
});
}
res.status(200).json({
token
});
try {
const dc = new Discord(json.access_token);
const dcUser = await dc.user();
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');
}
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();
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);
return res.status(500).json({
code: 500,
message: 'Internal Server Error'
});
}
}

View 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
View 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'
});
}
}

View file

@ -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.' });
}
}

View file

@ -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' });
}

View file

@ -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 db = new Database();
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'
});
}
}

View 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'
});
}

View 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
View 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);
}
}

View file

@ -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
});
}

View file

@ -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);
}

View file

@ -33,6 +33,59 @@ export default function Home() {
<p>
You can download it from Modrinth: <a href="https://modrinth.com/modpack/crsspack">https://modrinth.com/modpack/crsspack</a>.
</p>
<h2 id="rules">Rules</h2>
<ol>
<li>
The use of modified clients that give an unfair advantage to players, such as hacked clients, is not permitted.
<ul>
<li>
You are not allowed to use them even for their legitimate features, such as a fullbright option.
</li>
<li>
If admins suspect you are hacking you will be immediately banned.
</li>
</ul>
</li>
<li>
Do not modify or destroy (grief) other player&apos;s constructions without their consent, or steal any of their items.
<ul>
<li>
You are allowed to visit any build, as long as you don&apos;t take anything, and if you do you pay them back.
</li>
<li>
You should ask permission in the discord or the in-game chat before modifying builds.
</li>
</ul>
</li>
<li>
Follow the laws of the nations you are in to avoid issues with other players and making the server unfun to play.
<ul>
<li>
If you feel the laws are too vague, feel free to ask the people in charge of them what they meanr with something, and feel free to contribute to them. Complaining that they don&apos;t make sense won&apos;t get you anywhere.
</li>
<li>
Breaking laws isn&apos;t bannable, the nation you are in will take measures and punish you for your actions as they see fit.
</li>
</ul>
</li>
<li>
Do not attempt to make nations where the territory is already owned by another nation.
<ul>
<li>
You can make it near the borders of a nation but never inside one, you can&apos;t just take existing territory as your own.
</li>
<li>
Other nations are free to claim more territory whenever they feel like it, as long as it doesnt take other nations&apos; territory with it.
</li>
</ul>
</li>
</ol>
</PageContent>
<Footer />

View file

@ -1,138 +0,0 @@
import PageContent from '@/components/PageContent';
import NavBar from '@/components/NavBar';
import Meta from '@/components/Meta';
import styles from '../styles/PremiumPlus.module.scss';
import Link from 'next/link';
import Footer from '@/components/Footer';
export default function PremiumPlus() {
return (
<>
<Meta page={{ title: 'Premium Plus' }} />
<NavBar currentPage="pp" />
<PageContent>
<div className={styles.cardContainer}>
<div className={styles.card}>
<h1>CRSS Premium+ Subscription Plan</h1>
<span>
Unlock the Ultimate Minecraft Experience with CRSS Premium
</span>
<h2>$6.90/month</h2>
<span>or save 2% with annual billing!</span>
</div>
<span>
Join the elite ranks of CRSS Premium+ members and elevate your
Minecraft gameplay to new heights.
</span>
<span>
For just $6.90 per month, our subscription plan offers you exclusive
perks, priority access, and a superior gaming environment. With CRSS
Premium+, you&apos;ll enjoy premium features that enhance your
experience and set you apart from the rest.
</span>
<span>
<h3>Exclusive Benefits of CRSS Premium+:</h3>
<ul>
<li>
<strong>Priority Queue Access:</strong> Skip the lines and jump
straight into the game with priority server access, ensuring
you&apos;re always where the action is.
</li>
<li>
<strong>Custom Kits & Items:</strong> Receive unique in-game
kits and items available only to CRSS Premium+ members, giving
you an edge in every match.
</li>
<li>
<strong>Enhanced Gameplay Features:</strong> Access premium
plugins and gameplay enhancements, including advanced commands
and special game modes, designed to take your Minecraft
experience to the next level.
</li>
<li>
<strong>Monthly Loot Crates:</strong> Get monthly loot crates
filled with rare items, unique cosmetics, and special bonuses,
keeping your inventory well-stocked with top-tier gear.
</li>
<li>
<strong>Dedicated Support:</strong> Enjoy priority assistance
from our premium support team, ensuring any issues or questions
are resolved quickly so you can focus on the game.
</li>
<li>
<strong>VIP Events & Contests:</strong> Participate in exclusive
events and contests with bigger and better rewards, open only to
CRSS Premium+ members.
</li>
</ul>
<h3>Subscription Details:</h3>
<ul>
<li>
<strong>Subscription Fee:</strong> $6.90 per month, offering
exceptional value for a premium gaming experience.
</li>
<li>
<strong>Contract Term:</strong> Flexible monthly subscription
with the freedom to cancel anytime.
</li>
<li>
<strong>Instant Activation:</strong> Your CRSS Premium+ benefits
activate immediately upon subscription, so you can start
enjoying all the perks right away.
</li>
</ul>
<h3>Join CRSS Premium+ Today</h3>
<span>
For just $6.90 per month, you can access the ultimate Minecraft
experience with CRSS Premium+. Don&apos;t miss out on the
exclusive benefits that come with being a premium member of our
community. For more information or to subscribe, visit our website
at <Link href="/">crss.cc</Link> or contact our support team
at&nbsp;
<a href="mailto:admin@theclashfruit.me">admin@theclashfruit.me</a>
.
</span>
</span>
<center>
{' '}
<h2>Compare our plans</h2>
</center>
<div className={styles.subLayout}>
<div className={styles.card}>
<h1>CRSS Free</h1>
<ul>
<li>9 inventory slots</li>
<li>Full access to inventory</li>
<li>Unrestricted use of chat</li>
</ul>
<h3>Current Plan</h3>
</div>
<div className={styles.card}>
<h1>CRSS Premium+</h1>
<ul>
<li>Ad-free browsing</li>
<li>Access to special kits and gamemodes</li>
<li>Browse the web ingame</li>
<li>Exclusive capes</li>
</ul>
<h3>$6.90</h3>
<strong>Best Value</strong>
</div>
<div className={styles.card}>
<h1>CRSS EnterpriseX</h1>
<ul>
<li>Free flight</li>
<li>Creative mode</li>
<li>Add your own custom cape</li>
<li>Create your own items</li>
<li>Enterprise-grade management solutions</li>
</ul>
<h3>Contact Us</h3>
</div>
</div>
</div>
</PageContent>
<Footer />
</>
);
}

View file

@ -1,84 +0,0 @@
import AdBanner from '@/components/AdBanner';
import Footer from '@/components/Footer';
import Meta from '@/components/Meta';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
export default function Rules() {
return (
<>
<Meta page={{ title: 'Rules' }} />
<NavBar currentPage="rules" />
<PageContent>
<h1 id="rules">Rules</h1>
<ol>
<li>
The use of modified clients that give an unfair advantage to
players, such as hacked clients, is not permitted.
<ul>
<li>
You are not allowed to use them even for their legitimate
features, such as a &quot;fullbright&quot; option.
</li>
<li>
If admins suspect you are hacking you will be immediately
banned.
</li>
</ul>
</li>
<li>
Do not modify or destroy (grief) other player&apos;s constructions
without their consent, or steal any of their items.
<ul>
<li>
You are allowed to visit any build, as long as you don&apos;t
take anything, and if you do you pay them back.
</li>
<li>
You should ask permission in the discord or the in-game chat
before modifying builds.
</li>
</ul>
</li>
<li>
Follow the laws of the nations you are in to avoid issues with other
players and making the server not fun to play.
<ul>
<li>
If you feel the laws are too vague, feel free to ask the people
in charge of them what they mean with something, and feel free
to contribute to them. Complaining that they don&apos;t make
sense won&apos;t get you anywhere.
</li>
<li>
Breaking laws won&apos;t necessarily get you banned, the nation
you are in will take measures and punish you for your actions as
they see fit.
</li>
</ul>
</li>
<li>
Do not attempt to make nations where the territory is already owned
by another nation.
<ul>
<li>
You can make it near the borders of a nation but never inside
one, you can&apos;t just take existing territory as your own.
</li>
<li>
Other nations are free to claim more territory whenever they
feel like it, as long as it doesn&apos;t take other
nations&apos; territory with it.
</li>
</ul>
</li>
</ol>
</PageContent>
<Footer />
</>
);
}

File diff suppressed because it is too large Load diff

View file

@ -1,57 +0,0 @@
@import 'variables.module';
.card {
display: flex;
flex-direction: column;
align-items: center;
border-radius: 1.5rem;
padding: 1rem;
gap: 0.25rem;
h1 {
margin: 0px;
}
h2 {
margin: 0px;
}
* {
text-align: center;
}
}
.cardContainer {
display: flex;
flex-direction: column;
gap: 1rem;
}
div.card {
border: 1px solid $colorBorderLight1;
background: linear-gradient(
180deg,
$colorSurfaceLight1 0%,
$colorBorderLight1 200%
);
}
@media (prefers-color-scheme: dark) {
div.card {
border: 1px solid $colorBorderDark1;
background: linear-gradient(
180deg,
$colorSurfaceDark2 0%,
$colorBorderDark1 200%
);
}
}
.subLayout {
display: grid;
gap: 1rem;
}
@media (min-width: 1000px) {
div.subLayout {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}

View file

@ -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
View 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;
}

View file

@ -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
View 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
View 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}`;
}