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=
|
DB_PORT=
|
||||||
|
|
||||||
AUTH_SECRET=
|
AUTH_SECRET=
|
||||||
|
LINK_SECRET=
|
||||||
|
|
||||||
MC_API=
|
MC_API=
|
||||||
|
|
||||||
|
S3_REGION="fra1"
|
||||||
|
S3_ENDPOINT="https://fra1.digitaloceanspaces.com"
|
||||||
|
|
||||||
|
S3_ACCESS_KEY=
|
||||||
|
S3_SECRET_KEY=
|
|
@ -3,7 +3,8 @@
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": [
|
"indent": [
|
||||||
"error",
|
"error",
|
||||||
2
|
2,
|
||||||
|
{ "SwitchCase": 1 }
|
||||||
],
|
],
|
||||||
"quotes": [
|
"quotes": [
|
||||||
"error",
|
"error",
|
||||||
|
|
|
@ -8,6 +8,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint
|
||||||
steps:
|
steps:
|
||||||
- name: Deploy Over SSH
|
- name: Deploy Over SSH
|
||||||
uses: https://github.com/nekiro/ssh-job@main
|
uses: https://github.com/nekiro/ssh-job@main
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"tabWidth": 2,
|
|
||||||
"singleQuote": true,
|
|
||||||
"semi": true
|
|
||||||
}
|
|
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
|
@ -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}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"[typescriptreact]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
Home,
|
Home,
|
||||||
Scale,
|
|
||||||
AtSign,
|
AtSign,
|
||||||
Images,
|
Images,
|
||||||
Map,
|
Map,
|
||||||
|
@ -12,9 +11,7 @@ import {
|
||||||
LogOut,
|
LogOut,
|
||||||
LogIn,
|
LogIn,
|
||||||
Earth,
|
Earth,
|
||||||
X,
|
X
|
||||||
PlusIcon,
|
|
||||||
Plus,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
@ -31,19 +28,22 @@ import { useEffect, useRef, useState } from 'react';
|
||||||
import Dropdown from './Dropdown';
|
import Dropdown from './Dropdown';
|
||||||
import { useUser } from '@/context/UserContext';
|
import { useUser } from '@/context/UserContext';
|
||||||
|
|
||||||
import { Permission, hasPermission } from '@/utils/permissions';
|
import {
|
||||||
|
Permission,
|
||||||
|
hasPermission
|
||||||
|
} from '@/utils/permissions';
|
||||||
|
|
||||||
export default function NavBar({ currentPage }: { currentPage: string }) {
|
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 { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const server = {
|
const server = {
|
||||||
version: '1.16.5',
|
version: '1.15.2'
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildDiscordUrl = (): string => {
|
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('client_id', publicRuntimeConfig.discord.clientId);
|
||||||
url.searchParams.append('response_type', 'code');
|
url.searchParams.append('response_type', 'code');
|
||||||
url.searchParams.append(
|
url.searchParams.append('redirect_uri', publicRuntimeConfig.discord.redirectUri);
|
||||||
'redirect_uri',
|
url.searchParams.append('scope', publicRuntimeConfig.discord.scopes.join(' '));
|
||||||
publicRuntimeConfig.discord.redirectUri
|
|
||||||
);
|
|
||||||
url.searchParams.append(
|
|
||||||
'scope',
|
|
||||||
publicRuntimeConfig.discord.scopes.join(' ')
|
|
||||||
);
|
|
||||||
|
|
||||||
url.searchParams.append('state', router.asPath);
|
url.searchParams.append('state', router.asPath);
|
||||||
|
|
||||||
|
@ -77,17 +71,15 @@ export default function NavBar({ currentPage }: { currentPage: string }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="ip">Server Address:</label>
|
<label htmlFor="ip">
|
||||||
|
Server Address:
|
||||||
|
</label>
|
||||||
|
|
||||||
<input
|
<input type="text" value="play.crss.cc" id="ip" readOnly size={8} />
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -96,159 +88,120 @@ export default function NavBar({ currentPage }: { currentPage: string }) {
|
||||||
<nav className={`${styles.navBar} ${navOpen ? styles.navOpen : ''}`}>
|
<nav className={`${styles.navBar} ${navOpen ? styles.navOpen : ''}`}>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.navMobileContainer}>
|
<div className={styles.navMobileContainer}>
|
||||||
<button
|
<button className={styles.navToggle} onClick={() => { setNavOpen(!navOpen); }}>
|
||||||
className={styles.navToggle}
|
{ !navOpen ? <Menu /> : <X /> }
|
||||||
onClick={() => {
|
|
||||||
setNavOpen(!navOpen);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!navOpen ? <Menu /> : <X />}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.navCollapse}>
|
<div className={styles.navCollapse}>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link href={currentPage == 'home' ? '#' : '/'} className={currentPage == 'home' ? styles.active : ''}>
|
||||||
href={currentPage == 'home' ? '#' : '/'}
|
|
||||||
className={currentPage == 'home' ? styles.active : ''}
|
|
||||||
>
|
|
||||||
<Home />
|
<Home />
|
||||||
|
|
||||||
Home
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link href={currentPage == 'about' ? '#' : '/about'} className={currentPage == 'about' ? styles.active : ''}>
|
||||||
href={currentPage == 'rules' ? '#' : '/rules'}
|
|
||||||
className={currentPage == 'rules' ? styles.active : ''}
|
|
||||||
>
|
|
||||||
<Scale />
|
|
||||||
Rules
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
href={currentPage == 'about' ? '#' : '/about'}
|
|
||||||
className={currentPage == 'about' ? styles.active : ''}
|
|
||||||
>
|
|
||||||
<AtSign />
|
<AtSign />
|
||||||
|
|
||||||
About
|
About
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link href={currentPage == 'gallery' ? '#' : '/gallery'} className={currentPage == 'gallery' ? styles.active : ''}>
|
||||||
href={currentPage == 'gallery' ? '#' : '/gallery'}
|
|
||||||
className={currentPage == 'gallery' ? styles.active : ''}
|
|
||||||
>
|
|
||||||
<Images />
|
<Images />
|
||||||
|
|
||||||
Gallery
|
Gallery
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link href={currentPage == 'map' ? '#' : '/map'} className={currentPage == 'map' ? styles.active : ''}>
|
||||||
href={currentPage == 'map' ? '#' : '/map'}
|
|
||||||
className={currentPage == 'map' ? styles.active : ''}
|
|
||||||
>
|
|
||||||
<Map />
|
<Map />
|
||||||
|
|
||||||
Map
|
Map
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link href={currentPage == 'nations' ? '#' : '/nations'} className={currentPage == 'nations' ? styles.active : ''}>
|
||||||
href={currentPage == 'nations' ? '#' : '/nations'}
|
|
||||||
className={currentPage == 'nations' ? styles.active : ''}
|
|
||||||
>
|
|
||||||
<Earth />
|
<Earth />
|
||||||
|
|
||||||
Nations
|
Nations
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{isLoggedIn && user && (
|
{(isLoggedIn && user) && (
|
||||||
<li>
|
<li>
|
||||||
{(hasPermission(user.permissions, Permission.Admin) && (
|
{hasPermission(user.permissions, Permission.Admin) && (
|
||||||
<Dropdown
|
<Dropdown items={[
|
||||||
items={[
|
{
|
||||||
{
|
icon: User,
|
||||||
icon: User,
|
label: 'Profile',
|
||||||
label: 'Profile',
|
href: `/u/${user ? user.names.username : 'Loading...'}`
|
||||||
href: `/u/${
|
},
|
||||||
user ? user.names.username : 'Loading...'
|
{
|
||||||
}`,
|
icon: Settings,
|
||||||
},
|
label: 'Settings',
|
||||||
{
|
href: '/settings'
|
||||||
icon: Settings,
|
},
|
||||||
label: 'Settings',
|
{
|
||||||
href: '/settings',
|
divider: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Plus,
|
icon: LayoutDashboard,
|
||||||
label: 'Premium+',
|
label: 'Admin',
|
||||||
href: '/pp',
|
href: '/admin'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
divider: true,
|
icon: LogOut,
|
||||||
},
|
label: 'Logout',
|
||||||
{
|
onClick: async (e) => {
|
||||||
icon: LayoutDashboard,
|
e.preventDefault();
|
||||||
label: 'Admin',
|
|
||||||
href: '/admin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: LogOut,
|
|
||||||
label: 'Logout',
|
|
||||||
onClick: async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
await fetch('/api/v1/session', {
|
await fetch('/api/v1/session', {
|
||||||
method: 'DELETE',
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
||||||
router.reload();
|
router.reload();
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
]}
|
]} className={styles.dropDown}>
|
||||||
className={styles.dropDown}
|
|
||||||
>
|
|
||||||
<User />
|
<User />
|
||||||
|
|
||||||
{user ? user.names.global_name : 'Loading...'}
|
{user ? user.names.global_name : 'Loading...'}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)) || (
|
) || (
|
||||||
<Dropdown
|
<Dropdown items={[
|
||||||
items={[
|
{
|
||||||
{
|
icon: User,
|
||||||
icon: User,
|
label: 'Profile',
|
||||||
label: 'Profile',
|
href: `/u/${user ? user.names.username : 'Loading...'}`
|
||||||
href: `/u/${
|
},
|
||||||
user ? user.names.username : 'Loading...'
|
{
|
||||||
}`,
|
icon: Settings,
|
||||||
},
|
label: 'Settings',
|
||||||
{
|
href: '/settings'
|
||||||
icon: Settings,
|
},
|
||||||
label: 'Settings',
|
{
|
||||||
href: '/settings',
|
divider: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
divider: true,
|
icon: LogOut,
|
||||||
},
|
label: 'Logout',
|
||||||
{
|
onClick: async (e) => {
|
||||||
icon: LogOut,
|
e.preventDefault();
|
||||||
label: 'Logout',
|
|
||||||
onClick: async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
await fetch('/api/v1/session', {
|
await fetch('/api/v1/session', {
|
||||||
method: 'DELETE',
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
||||||
router.reload();
|
router.reload();
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
]}
|
]} className={styles.dropDown}>
|
||||||
className={styles.dropDown}
|
|
||||||
>
|
|
||||||
<User />
|
<User />
|
||||||
|
|
||||||
{user ? user.names.global_name : 'Loading...'}
|
{user ? user.names.global_name : 'Loading...'}
|
||||||
|
@ -256,10 +209,11 @@ export default function NavBar({ currentPage }: { currentPage: string }) {
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{!isLoggedIn && !user && (
|
{(!isLoggedIn && !user) && (
|
||||||
<li>
|
<li>
|
||||||
<Link href={buildDiscordUrl()}>
|
<Link href={buildDiscordUrl()}>
|
||||||
<LogIn />
|
<LogIn />
|
||||||
|
|
||||||
Login
|
Login
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
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;
|
381
lib/Database.ts
381
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 mysql, { Pool, QueryResult } from 'mysql2/promise';
|
||||||
|
import Discord from './Discord';
|
||||||
import crypto from 'node:crypto';
|
import { createSessionToken, DiscordTokenData } from '@/utils/token_util';
|
||||||
|
import snowflake from '@/utils/snowflake';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
static instance: (Database | null) = null;
|
static instance: (Database | null) = null;
|
||||||
|
private mysqlPool: (Pool | null) = null;
|
||||||
mysqlPool: (Pool | null) = null;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if(Database.instance)
|
if(Database.instance)
|
||||||
|
@ -72,153 +28,252 @@ class Database {
|
||||||
Database.instance = this;
|
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> {
|
if((rows as any[]).length === 0)
|
||||||
const [ result ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE did = ?', [ user.id ]);
|
return undefined;
|
||||||
|
|
||||||
if ((result as any).length > 0) {
|
const row = (rows as any[])[0];
|
||||||
return (result as any)[0].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [ res ] = await this.mysqlPool!.execute('INSERT INTO users (did, username, global_name, email, avatar, banner, accent_color) VALUES (?, ?, ?, ?, ?, ?, ?)', [
|
return {
|
||||||
user.id,
|
id: row.id as BigInt,
|
||||||
user.username,
|
|
||||||
(user.global_name || user.username),
|
|
||||||
user.email,
|
|
||||||
user.avatar,
|
|
||||||
user.banner,
|
|
||||||
user.accent_color
|
|
||||||
]);
|
|
||||||
|
|
||||||
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.
|
* @returns User
|
||||||
* @param userAgent The user agent of the user.
|
|
||||||
*
|
|
||||||
* @returns `string` The session token.
|
|
||||||
*/
|
*/
|
||||||
async createSession(userData: DiscordTokenData, userAgent?: string): Promise<string | null> {
|
async getUserUsername(username: string): Promise<User | undefined> {
|
||||||
const res = await fetch(`${process.env.DISCORD_API}/users/@me`, {
|
const [ rows ] = await this.mysqlPool!.query('SELECT * FROM users WHERE username = ?', [ username ]);
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${userData.access_token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
if((rows as any[]).length === 0)
|
||||||
const user = await res.json();
|
return undefined;
|
||||||
const uid = await this.createUser(user);
|
|
||||||
|
|
||||||
const sum = crypto.createHmac('sha256', process.env.AUTH_SECRET!);
|
const row = (rows as any[])[0];
|
||||||
const base = Buffer.from(user.id).toString('base64').replaceAll('=', '');
|
|
||||||
const date = Buffer.from((Date.now() - 1688940000000).toString()).toString('base64').replaceAll('=', '');
|
|
||||||
|
|
||||||
sum.update(userData.access_token);
|
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 (?, ?, ?, ?, ?, ?, ?)', [
|
email: row.email,
|
||||||
sid,
|
|
||||||
uid,
|
avatar: row.avatar,
|
||||||
userData.access_token,
|
banner: row.banner,
|
||||||
userData.refresh_token,
|
|
||||||
userData.id_token,
|
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,
|
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;
|
return undefined;
|
||||||
} else {
|
|
||||||
throw new Error('Error Fetching Discord User Data');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSession(sid: string): Promise<void> {
|
async deleteSession(id: string): Promise<boolean> {
|
||||||
await this.mysqlPool!.execute('DELETE FROM sessions WHERE sid = ?', [ sid ]);
|
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> {
|
async getSession(token: string): Promise<any | undefined> {
|
||||||
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM sessions WHERE sid = ?', [ sid ]);
|
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> {
|
return rows as any[];
|
||||||
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE id = ?', [ id ]);
|
|
||||||
|
|
||||||
return (rows as UserTable[])[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUsers(): Promise<UserTableWithoutEmail[]> {
|
// Meta ----------------
|
||||||
const [ rows ] = await this.mysqlPool!.execute('SELECT id, did, username, global_name, avatar, banner, accent_color, permissions FROM users');
|
|
||||||
|
|
||||||
return rows as UserTableWithoutEmail[];
|
async getTeam(): Promise<(TeamMember | undefined)[]> {
|
||||||
}
|
const [ rows ] = await this.mysqlPool!.query('SELECT * FROM team');
|
||||||
|
|
||||||
async getUserUsername(username: string): Promise<UserTable> {
|
const usersPromises = (rows as any[]).map(row => this.getUser(row.user_id));
|
||||||
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE username = ?', [ username ]);
|
const users = await Promise.all(usersPromises);
|
||||||
|
|
||||||
return (rows as UserTable[])[0];
|
return users.map((user, i) => {
|
||||||
}
|
if (!user) return undefined;
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
id: row.tid,
|
...user,
|
||||||
uid: row.uid,
|
role: (rows as any[])[i].role
|
||||||
did: row.did,
|
} as TeamMember;
|
||||||
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),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
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"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.637.0",
|
||||||
"@icons-pack/react-simple-icons": "^10.0.0",
|
"@icons-pack/react-simple-icons": "^10.0.0",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
"lucide-react": "^0.429.0",
|
"lucide-react": "^0.429.0",
|
||||||
"mysql2": "^3.11.0",
|
"mysql2": "^3.11.0",
|
||||||
"next": "14.2.6",
|
"next": "14.2.6",
|
||||||
|
"nodejs-snowflake": "^2.0.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
|
|
|
@ -7,7 +7,7 @@ import Database from '@/lib/Database';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Role } from '@/utils/permissions';
|
import { TeamRole } from '@/utils/badges';
|
||||||
|
|
||||||
import styles from '@/styles/About.module.scss';
|
import styles from '@/styles/About.module.scss';
|
||||||
import { Globe } from 'lucide-react';
|
import { Globe } from 'lucide-react';
|
||||||
|
@ -47,8 +47,8 @@ export default function About({ teamMembers }: { teamMembers: any[] }) {
|
||||||
|
|
||||||
<h3>{member.global_name}</h3>
|
<h3>{member.global_name}</h3>
|
||||||
|
|
||||||
{member.role === Role.Owner && <label>Owner</label>}
|
{member.role === TeamRole.Owner && <label>Owner</label>}
|
||||||
{member.role === Role.Admin && <label>Admin</label>}
|
{member.role === TeamRole.Admin && <label>Admin</label>}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</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,71 +1,78 @@
|
||||||
import Database from '@/lib/Database';
|
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';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
type Data = {
|
|
||||||
sid: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Error = {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse<Data | Error>,
|
res: NextApiResponse<any>,
|
||||||
) {
|
) {
|
||||||
const db = new Database();
|
const db = new Database();
|
||||||
|
const s3 = new S3Storage();
|
||||||
|
|
||||||
const { code, state } = req.query;
|
const { code, state } = req.query;
|
||||||
|
|
||||||
console.log(code, state);
|
if (typeof code !== 'string' || typeof state !== 'string') {
|
||||||
|
return res.status(400).json({
|
||||||
const discordApi = process.env.DISCORD_API!;
|
code: 400,
|
||||||
|
message: 'Invalid Request'
|
||||||
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' }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(400).json(
|
try {
|
||||||
{ error: 'Invalid code' }
|
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'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
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 Database from '@/lib/Database';
|
||||||
|
import { getAuthenticatedUser } from '@/utils/auth_util';
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
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(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse<Data | Error>,
|
res: NextApiResponse<User | ErrorResponse>,
|
||||||
) {
|
) {
|
||||||
const db = new Database();
|
const db = new Database();
|
||||||
const sid = req.cookies.session;
|
const user = await getAuthenticatedUser(req);
|
||||||
|
|
||||||
if (!sid)
|
if (!user)
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
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)
|
if (req.method === 'PATCH') {
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
// TODO: implement
|
||||||
|
|
||||||
const user = await db.getUser(session.uid);
|
return res.status(501).json({
|
||||||
|
code: 501,
|
||||||
|
message: 'Not Yet Implemented'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).json({
|
if (req.method === 'DELETE') {
|
||||||
id: user.id,
|
// TODO: implement
|
||||||
discord_id: user.did,
|
|
||||||
names: {
|
return res.status(501).json({
|
||||||
username: user.username,
|
code: 501,
|
||||||
global_name: user.global_name
|
message: 'Not Yet Implemented'
|
||||||
},
|
});
|
||||||
email: user.email,
|
}
|
||||||
avatar: user.avatar,
|
|
||||||
banner: user.banner,
|
return res.status(405).json({
|
||||||
accent_color: user.accent_color,
|
code: 405,
|
||||||
permissions: user.permissions
|
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);
|
|
||||||
}
|
|
|
@ -33,6 +33,59 @@ export default function Home() {
|
||||||
<p>
|
<p>
|
||||||
You can download it from Modrinth: <a href="https://modrinth.com/modpack/crsspack">https://modrinth.com/modpack/crsspack</a>.
|
You can download it from Modrinth: <a href="https://modrinth.com/modpack/crsspack">https://modrinth.com/modpack/crsspack</a>.
|
||||||
</p>
|
</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'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'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't make sense won't get you anywhere.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Breaking laws isn'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'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' territory with it.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
138
pages/pp.tsx
138
pages/pp.tsx
|
@ -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'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'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'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
|
|
||||||
<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 />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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 "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'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'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't make
|
|
||||||
sense won't get you anywhere.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Breaking laws won'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'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't take other
|
|
||||||
nations' territory with it.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</PageContent>
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
7065
pnpm-lock.yaml
7065
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,8 @@
|
||||||
import Database from '@/lib/Database';
|
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();
|
const db = new Database();
|
||||||
|
|
||||||
|
@ -24,6 +27,49 @@ export async function isUserAdmin(sid?: string): Promise<doas> {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
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 {
|
export enum Permission {
|
||||||
Admin = 1 << 0
|
SuperAdmin = 1 << 0,
|
||||||
|
Admin = 1 << 1,
|
||||||
|
ServerPlayer = 1 << 2
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Role {
|
export enum PermissionNamed {
|
||||||
Owner = 'owner',
|
SuperAdmin = 'super_admin',
|
||||||
Admin = '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 {
|
export function hasPermission(permissions: number, permission: Permission): boolean {
|
||||||
return (permissions & permission) === permission;
|
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