Andriy 2022-12-23 15:53:09 +02:00
parent dd1aab37d0
commit 8ec3e6ff55
60 changed files with 5498 additions and 565 deletions

5
.gitignore vendored
View File

@ -1,4 +1 @@
node_modules/
dist/
.env
README.md
node_modules/

BIN
auth_concept.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

4
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
dist/
.env
README.md

48
backend/package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "rpz_auth",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"generate": "prisma generate",
"migrate": "prisma migrate deploy",
"build": "rm -rf dist/** && tsc",
"dev": "tsc -w",
"start": "node ./dist"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@tsconfig/node18": "^1.0.1",
"@types/jsonwebtoken": "^8.5.9",
"@types/minio": "^7.0.15",
"@types/node": "^18.11.17",
"@types/passport-local": "^1.0.34",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0",
"eslint": "^8.30.0",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.8.1",
"typescript": "^4.9.4"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.236.0",
"@fastify/autoload": "^5.6.0",
"@fastify/cookie": "^8.3.0",
"@fastify/passport": "^2.2.0",
"@fastify/secure-session": "^5.3.0",
"@prisma/client": "^4.8.0",
"axios": "^0.27.2",
"discord-api-types": "^0.37.24",
"fastify": "^4.10.2",
"json-schema-to-ts": "^2.6.2",
"jsonwebtoken": "^8.5.1",
"minio": "^7.0.32",
"nanoid": "^3.3.4",
"passport-local": "^1.0.0",
"prisma": "^4.8.0",
"redis": "^4.5.1",
"simple-oauth2": "^4.3.0"
}
}

1694
backend/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "hasPass" BOOLEAN NOT NULL DEFAULT false;

View File

@ -0,0 +1,28 @@
-- CreateTable
CREATE TABLE "Season" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"starts" TIMESTAMP(3),
"ends" TIMESTAMP(3),
CONSTRAINT "Season_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"creationDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isBanned" BOOLEAN NOT NULL DEFAULT false,
"bannedDate" TIMESTAMP(3),
"bannedReason" TEXT,
"seasonId" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_seasonId_fkey" FOREIGN KEY ("seasonId") REFERENCES "Season"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,11 @@
/*
Warnings:
- You are about to drop the column `hasPass` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Season" ADD COLUMN "current" BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE "User" DROP COLUMN "hasPass";

View File

@ -0,0 +1,11 @@
/*
Warnings:
- A unique constraint covering the columns `[discordId]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "discordId" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "User_discordId_key" ON "User"("discordId");

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "tokenVersion" INTEGER NOT NULL DEFAULT 0;

View File

@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `form` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "form" JSONB NOT NULL;

View File

@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `form` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "form";

View File

@ -0,0 +1,22 @@
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "guildId" TEXT;
-- CreateTable
CREATE TABLE "Guild" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL,
"discordRole" TEXT NOT NULL,
"leaderId" TEXT NOT NULL,
CONSTRAINT "Guild_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Guild_leaderId_key" ON "Guild"("leaderId");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Guild" ADD CONSTRAINT "Guild_leaderId_fkey" FOREIGN KEY ("leaderId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,15 @@
/*
Warnings:
- You are about to drop the `Link` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Link" DROP CONSTRAINT "Link_sessionId_fkey";
-- DropTable
DROP TABLE "Link";
-- DropTable
DROP TABLE "Session";

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "password" TEXT;

View File

@ -0,0 +1,67 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Season {
id String @id
name String
starts DateTime?
ends DateTime?
accounts Account[]
current Boolean @default(true)
}
model Account {
id String @id @default(uuid())
user User @relation(fields: [userId], references: [id])
userId String
creationDate DateTime @default(now())
isBanned Boolean @default(false)
bannedDate DateTime?
bannedReason String?
season Season? @relation(fields: [seasonId], references: [id])
seasonId String?
guild Guild? @relation(fields: [guildId], references: [id])
guildId String?
leaderOf Guild? @relation(name: "guildLeader")
}
model Guild {
id String @id
name String
description String
discordRole String
leader Account @relation(name: "guildLeader", fields: [leaderId], references: [id])
leaderId String @unique
members Account[]
}
model User {
id String @id @default(uuid())
nickname String? @unique
discordId String? @unique
accounts Account[]
tokenVersion Int @default(0)
password String?
}
// model Session {
// id String @id @default(uuid())
// nickname String
// ip String
// verified Boolean @default(false)
// expiredAfter DateTime
// link Link?
// }
// model Link {
// id String @id
// session Session @relation(fields: [sessionId], references: [id])
// sessionId String @unique
// link String
// }

View File

@ -1,17 +1,28 @@
import fastify from 'fastify';
import fastifyAutoload from '@fastify/autoload';
import { PrismaClient } from '@prisma/client';
// import { Client as MinioClient } from 'minio';
import { S3Client } from '@aws-sdk/client-s3';
import fastifyCookie from '@fastify/cookie';
// import { readFile } from 'fs/promises';
// import { createClient, RedisClientType } from 'redis';
// import fastifySchedule from '@fastify/schedule';
// import fastifyCors from '@fastify/cors';
import path from 'path';
import { env } from 'process';
// import { FromSchema } from 'json-schema-to-ts';
declare module 'fastify' {
interface FastifyInstance {
db: PrismaClient;
storage: S3Client;
// redis: RedisClientType;
}
interface FastifyReply {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendError: (error: any, code?: number, explanation?: string) => void;
}
}
declare global {
@ -25,6 +36,11 @@ declare global {
AUTH_CLIENT_SECRET: string;
REDIS_URL: string;
URL_TOKEN: string;
S3_ENDPOINT: string;
S3_ID: string;
S3_KEY: string;
AUTH_SECRET: string;
AUTH_SALT: string;
}
}
}
@ -34,6 +50,18 @@ fastify({
ajv: { customOptions: { allErrors: true } }
})
.decorate('db', new PrismaClient())
.decorate(
'storage',
new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: 'us-east',
credentials: {
accessKeyId: process.env.S3_ID,
secretAccessKey: process.env.S3_KEY
}
})
)
.register(fastifyCookie)
// .decorate('redis', async () => {
// const client = createClient({
// url: process.env.REDIS_URL
@ -49,18 +77,30 @@ fastify({
// origin: ['http://127.0.0.1:3000'],
// credentials: true
// })
.decorateReply(
'sendError',
function (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any,
code = 500,
explanation = 'Unexpected error occured. Issue will be reported to staff.'
) {
this.log.error(error);
this.code(code).send(new Error(explanation));
}
)
.register(fastifyAutoload, {
dir: path.join(__dirname, 'routes'),
routeParams: true,
autoHooks: true,
options: { prefix: '/api' }
})
.register(fastifyAutoload, {
dir: path.join(__dirname, 'link'),
routeParams: true,
autoHooks: true,
options: { prefix: '/a' }
})
// .register(fastifyAutoload, {
// dir: path.join(__dirname, 'link'),
// routeParams: true,
// autoHooks: true,
// options: { prefix: '/a' }
// })
.listen({ port: process.env.PORT || 8080, host: '0.0.0.0' })
// eslint-disable-next-line no-console
.then(() => console.log('Server Started'))

View File

@ -20,30 +20,32 @@ declare module 'fastify' {
export default async (server: FastifyInstance) =>
server
.decorateReply('sendTokens', function (this: FastifyReply, { id, nickname }: User) {
return this.setCookie(
'owo',
jwt.sign({ tokenVersion: 0, id }, process.env.JWT_REFRESH_SECRET, {
expiresIn: '30d'
}),
cookieSettings
).send({
user: {
id,
nickname
},
token: jwt.sign(
{
.decorateReply(
'sendTokens',
function (this: FastifyReply, { id, nickname, tokenVersion }: User) {
return this.setCookie(
'uwu',
jwt.sign({ tokenVersion, id }, process.env.JWT_REFRESH_SECRET, {
expiresIn: '30d'
}),
cookieSettings
).send({
user: {
id,
nickname
},
process.env.JWT_ACCESS_SECRET,
{
expiresIn: '15m'
}
)
});
})
token: jwt.sign(
{
id
},
process.env.JWT_ACCESS_SECRET,
{
expiresIn: '15m'
}
)
});
}
)
.decorateReply('clearTokens', function (this: FastifyReply) {
return this.clearCookie('owo', cookieSettings);
return this.clearCookie('uwu', cookieSettings);
});

View File

@ -0,0 +1,44 @@
import { FromSchema } from 'json-schema-to-ts';
import { FastifyInstance } from 'fastify';
import { stringify } from 'querystring';
import axios from 'axios';
import { pbkdf2 } from 'node:crypto';
import { promisify } from 'node:util';
const schema = {
body: {
type: 'object',
required: ['username', 'password'],
properties: {
username: { type: 'string' },
password: { type: 'string' }
}
}
} as const;
export default async (server: FastifyInstance) =>
server.post<{ Body: FromSchema<typeof schema.body> }>(
'/login',
{ schema },
async (req, reply) => {
const pbkdf2Promise = promisify(pbkdf2);
const hash = await pbkdf2Promise(
req.body.password,
process.env.AUTH_SALT,
100000,
64,
'sha512'
);
await server.db.user
.findFirst({
where: {
nickname: req.body.username,
password: hash.toString('hex')
}
})
.then(user => {
if (!user) return reply.code(403).send(new Error('Invalid username or password'));
reply.sendTokens(user);
});
}
);

View File

@ -0,0 +1,4 @@
import { FastifyInstance } from 'fastify';
export default async (server: FastifyInstance) =>
server.post('/logout', async (_req, reply) => reply.clearTokens().send());

View File

@ -0,0 +1,20 @@
import { FastifyInstance } from 'fastify';
import { verify } from 'jsonwebtoken';
import { User } from '@prisma/client';
export default async (server: FastifyInstance) =>
server.post('/refresh_token', async (req, reply) => {
const refreshToken = req.cookies.uwu ?? null;
if (!refreshToken) return reply.send({ error: 'uwu cookie is lost' });
try {
const { id, tokenVersion } = verify(refreshToken, process.env.JWT_REFRESH_SECRET) as User,
user = await server.db.user.findUnique({
where: { id }
});
if (!user || tokenVersion !== user.tokenVersion)
return reply.code(400).clearTokens().send(new Error('Please login again'));
return reply.sendTokens(user);
} catch (e) {
return reply.clearTokens().sendError(e, 400, 'Cannot validate your auth, please login again');
}
});

View File

@ -0,0 +1,60 @@
import { FromSchema } from 'json-schema-to-ts';
import { FastifyInstance } from 'fastify';
import { pbkdf2 } from 'node:crypto';
import { promisify } from 'node:util';
const schema = {
body: {
type: 'object',
required: ['username', 'password', 'key'],
properties: {
username: { type: 'string' },
password: { type: 'string' },
key: { type: 'string' }
}
}
} as const;
export default async (server: FastifyInstance) =>
server.post<{ Body: FromSchema<typeof schema.body> }>(
'/register',
{ schema },
async (req, reply) => {
if (req.body.key !== process.env.AUTH_SECRET)
return reply.code(403).send('Invalid register key');
const currentSeason = (
await server.db.season.findFirst({ where: { current: true }, select: { id: true } })
)?.id;
const pbkdf2Promise = promisify(pbkdf2);
const hash = await pbkdf2Promise(
req.body.password,
process.env.AUTH_SALT,
100000,
64,
'sha512'
);
server.log.info(reply.sent);
await server.db.user
.create({
data: {
nickname: req.body.username,
password: hash.toString('hex'),
accounts: {
create: {
season: {
connect: {
id: currentSeason
}
}
}
}
}
})
.then(user => reply.sendTokens(user))
.catch(err => {
if (err.code === 'P2002') reply.code(400).send('User is already exists');
reply.sendError(err);
});
}
);

View File

@ -0,0 +1,45 @@
import { FromSchema } from 'json-schema-to-ts';
import { FastifyInstance } from 'fastify';
import { stringify } from 'querystring';
import axios from 'axios';
import { pbkdf2 } from 'node:crypto';
import { promisify } from 'node:util';
const schema = {
body: {
type: 'object',
required: ['username', 'password'],
properties: {
username: { type: 'string' },
password: { type: 'string' }
}
}
} as const;
export default async (server: FastifyInstance) =>
server.post<{ Body: FromSchema<typeof schema.body> }>(
'/verify',
{ schema },
async (req, reply) => {
const pbkdf2Promise = promisify(pbkdf2);
const hash = await pbkdf2Promise(
req.body.password,
process.env.AUTH_SALT,
100000,
64,
'sha512'
);
await server.db.user
.findFirst({
where: {
nickname: req.body.username,
password: hash.toString('hex')
}
})
.then(user => {
if (!user) return reply.code(403).send(new Error('Invalid username or password'));
reply.send('ok');
});
}
);

View File

@ -0,0 +1,11 @@
import { FastifyInstance } from 'fastify';
export default async (server: FastifyInstance) =>
server.get('/health', (req, reply) => {
try {
server.db.$queryRaw`SELECT 1`;
reply.send('ok');
} catch {
reply.send('bad');
}
});

View File

@ -0,0 +1,59 @@
import axios from 'axios';
import { FastifyInstance } from 'fastify';
import { HeadObjectCommand } from '@aws-sdk/client-s3';
export default async (server: FastifyInstance) =>
server.get<{ Params: { nickname: string } }>('/:nickname', async (req, reply) => {
const existingUser = await server.db.account.findFirst({
where: {
user: {
nickname: req.params.nickname
},
season: {
current: true
}
},
include: {
user: {
select: {
nickname: true
}
},
guild: {
select: {
id: true
}
}
}
});
const existingSkin = await server.storage
.send(new HeadObjectCommand({ Bucket: 'rpz', Key: `skins/${req.params.nickname}.png` }))
.then(head => head)
.catch(() => null);
console.log(req.params.nickname);
if (existingUser && existingSkin) {
reply.send({
SKIN: {
url: `${process.env.S3_ENDPOINT}/rpz/skins/${existingUser.user.nickname}.png`
},
CAPE: existingUser.guild
? {
url: `${process.env.S3_ENDPOINT}/rpz/capes/${existingUser.guild.id}.png`
}
: undefined
});
} else {
const {
data: { id: uuid }
} = await axios.get(`https://api.mojang.com/users/profiles/minecraft/${req.params.nickname}`);
const { data: skinResponseRaw } = await axios.get(
`https://sessionserver.mojang.com/session/minecraft/profile/${uuid}`
);
const skinResponse = JSON.parse(
Buffer.from(skinResponseRaw.properties[0].value, 'base64').toString('utf-8')
);
// console.log(skinResponse);
reply.send(skinResponse.textures);
}
});

View File

@ -0,0 +1,21 @@
import { FastifyInstance } from 'fastify';
import { FromSchema } from 'json-schema-to-ts';
import { nanoid } from 'nanoid/async';
const schema = {
body: {
type: 'object',
required: ['session', 'key'],
properties: {
session: { type: 'string' },
key: { type: 'string' }
}
}
} as const;
export default async (server: FastifyInstance) =>
server.post<{ Body: FromSchema<typeof schema.body> }>(
'/create',
{ schema },
async (req, reply) => {}
);

View File

@ -0,0 +1,11 @@
import { FastifyInstance } from 'fastify';
export default async (server: FastifyInstance) =>
server.get<{ Params: { id: string } }>('/:id', (req, reply) =>
server.db.user
.findFirstOrThrow({ where: { id: req.params.id }, select: {} })
.then(user => reply.send(user))
.catch(() => server.db.user.findFirstOrThrow({ where: { nickname: req.params.id } }))
.then(user => reply.send(user))
.catch(() => reply.code(404).send('User not found'))
);

View File

@ -1,9 +1,9 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
"outDir": "dist",
"resolveJsonModule": true
// "module": "esnext"
},
"esModuleInterop": true,
"resolveJsonModule": true
"esModuleInterop": true
}

13
frontend/.eslintignore Normal file
View File

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

20
frontend/.eslintrc.cjs Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
}
};

8
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example

1
frontend/.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

13
frontend/.prettierignore Normal file
View File

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

9
frontend/.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

38
frontend/README.md Normal file
View File

@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

32
frontend/package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "frontend",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.6.2",
"prettier-plugin-svelte": "^2.7.0",
"svelte": "^3.44.0",
"svelte-check": "^2.7.1",
"svelte-preprocess": "^4.10.6",
"tslib": "^2.3.1",
"typescript": "^4.7.4",
"vite": "^3.1.0"
},
"type": "module"
}

9
frontend/src/app.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Locals {}
// interface PageData {}
// interface Error {}
// interface Platform {}
}

12
frontend/src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body>
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -0,0 +1,2 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>

BIN
frontend/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

15
frontend/svelte.config.js Normal file
View File

@ -0,0 +1,15 @@
import adapter from '@sveltejs/adapter-auto';
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess(),
kit: {
adapter: adapter()
}
};
export default config;

17
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

8
frontend/vite.config.ts Normal file
View File

@ -0,0 +1,8 @@
import { sveltekit } from '@sveltejs/kit/vite';
import type { UserConfig } from 'vite';
const config: UserConfig = {
plugins: [sveltekit()]
};
export default config;

View File

@ -1,40 +0,0 @@
{
"name": "rpz_auth",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"generate": "prisma generate",
"migrate": "prisma migrate deploy",
"build": "rm -rf dist/** && tsc",
"dev": "tsc -w",
"start": "node ./dist"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@tsconfig/node18": "^1.0.1",
"@types/jsonwebtoken": "^8.5.9",
"@types/node": "^18.7.13",
"@typescript-eslint/eslint-plugin": "^5.36.0",
"@typescript-eslint/parser": "^5.35.1",
"eslint": "^8.23.0",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.7.1",
"typescript": "^4.8.2"
},
"dependencies": {
"@fastify/autoload": "^5.2.0",
"@fastify/cookie": "^8.1.0",
"@prisma/client": "^4.2.1",
"axios": "^0.27.2",
"fastify": "^4.5.3",
"json-schema-to-ts": "^2.5.5",
"jsonwebtoken": "^8.5.1",
"nanoid": "3",
"prisma": "^4.3.0",
"redis": "^4.3.0",
"simple-oauth2": "^4.3.0"
}
}

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,3 @@
packages:
- 'backend'
- 'frontend'

View File

@ -1,29 +0,0 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id
nickname String? @unique
}
model Session {
id String @id @default(uuid())
nickname String
ip String
verified Boolean @default(false)
expiredAfter DateTime
link Link?
}
model Link {
id String @id
session Session @relation(fields: [sessionId], references: [id])
sessionId String @unique
link String
}

View File

@ -1,21 +0,0 @@
import { PrismaClient } from '@prisma/client';
import('nanoid').then(nanoid => {
const db = new PrismaClient();
db.session
.create({
data: {
id: nanoid.nanoid(10),
ip: '127.0.0.1',
nickname: 'Qugalet',
expiredAfter: new Date(Date.now() + 604800)
}
})
.then(session =>
console.log(
encodeURI(
`https://auth.m0e.space/application/o/authorize/?client_id=7f24f967b2faf3d8c1c53771661abff8e07060c0&response_type=code&redirect_uri=http://localhost:8080/api/auth/verify?session=${session.id}&scope=profile openid`
)
)
);
});

View File

@ -1,76 +0,0 @@
import { FastifyInstance } from 'fastify';
import { FromSchema } from 'json-schema-to-ts';
import { nanoid } from 'nanoid/async';
const schema = {
querystring: {
type: 'object',
required: ['session', 'key'],
properties: {
session: { type: 'string' },
key: { type: 'string' }
}
}
} as const;
export default async (server: FastifyInstance) =>
server.get<{ Querystring: FromSchema<typeof schema.querystring> }>(
'/create',
async (req, reply) => {
if (req.query.key !== process.env.URL_TOKEN) return reply.code(403).send('Forbidden');
const session = await server.db.session.findFirst({
where: {
id: req.query.session
}
});
if (!session) return reply.code(404).send('Session is not found');
const linkedSession = await server.db.link.findFirst({
where: { session: { id: req.query.session } },
select: {
id: true
}
});
if (linkedSession)
reply.send(
`${
process.env.NODE_ENV === 'production'
? 'https://mc.m0e.space/a'
: 'http://localhost:8080/a'
}/${linkedSession.id}`
);
let linkId = await nanoid(5);
// eslint-disable-next-line no-constant-condition
while (true) {
const link = await server.db.link.findFirst({
where: { id: linkId },
select: { id: true }
});
if (!link) break;
linkId = await nanoid(5);
}
await server.db.link.create({
data: {
id: linkId,
link: encodeURI(
`https://auth.m0e.space/application/o/authorize/?client_id=7f24f967b2faf3d8c1c53771661abff8e07060c0&response_type=code&redirect_uri=${
process.env.NODE_ENV === 'production'
? 'https://mc.m0e.space'
: 'http://localhost:8080'
}/api/auth/verify?session=${req.query.session}&scope=profile openid`
),
session: {
connect: {
id: req.query.session
}
}
}
});
reply.send(
`${
process.env.NODE_ENV === 'production'
? 'https://mc.m0e.space/a'
: 'http://localhost:8080/a'
}/${linkId}`
);
}
);

View File

@ -1,9 +0,0 @@
import { FastifyInstance } from 'fastify';
export default async (server: FastifyInstance) =>
server.get<{ Params: { id: string } }>('/:id', (req, reply) =>
server.db.link
.findFirstOrThrow({ where: { id: req.params.id } })
.then(link => reply.redirect(link.link))
.catch(() => reply.code(404).send('URL not found'))
);

View File

@ -1,88 +0,0 @@
import { FastifyInstance } from 'fastify';
import { FromSchema } from 'json-schema-to-ts';
import { stringify } from 'querystring';
import axios from 'axios';
const schema = {
querystring: {
type: 'object',
required: ['code', 'session'],
properties: {
code: { type: 'string' },
session: { type: 'string' }
}
}
} as const;
export default async (server: FastifyInstance) =>
server.get<{ Querystring: FromSchema<typeof schema.querystring> }>(
'/verify',
{ schema },
(req, reply) => {
axios
.post(
'https://auth.m0e.space/application/o/token/',
stringify({
client_id: process.env.AUTH_CLIENT_ID,
client_secret: process.env.AUTH_CLIENT_SECRET,
grant_type: 'authorization_code',
redirect_uri:
process.env.NODE_ENV === 'production'
? 'https://mc.m0e.space/api/auth/verify'
: 'http://localhost:8080/api/auth/verify',
code: req.query.code
})
)
.then(res =>
axios
.get('https://auth.m0e.space/application/o/userinfo/', {
headers: { Authorization: `Bearer ${res.data.access_token}` }
})
.then(async res => {
const user =
(await server.db.user.findFirst({ where: { id: res.data.sub } })) ||
(await server.db.user.create({ data: { id: res.data.sub } }));
const session = await server.db.session.findFirst({
where: { id: req.query.session }
});
if (!session) return reply.code(400).send('Invalid session');
const userByNickname = await server.db.user.findFirst({
where: { nickname: session.nickname }
});
if (!userByNickname) {
await Promise.all([
server.db.user.update({
where: { id: user.id },
data: { nickname: session.nickname }
}),
server.db.session.update({
where: { id: req.query.session },
data: {
verified: true
}
})
]).then(() => reply.redirect('https://mc.m0e.space/message/success'));
// await server.redis.publish('rpz_auth', req.query.session);
} else if (userByNickname.id !== user.id) reply.send(403).send('Forbidden');
else
await server.db.session
.update({
where: { id: req.query.session },
data: {
verified: true
}
})
.then(() => reply.redirect('https://mc.m0e.space/message/success'));
})
.catch(err => {
console.log(err);
reply.code(500).send(err);
})
)
.catch(err => {
console.log(err);
reply.code(500).send(err);
});
}
);