chore: Move API source code to API folder.

This commit is contained in:
greysoh 2024-04-28 14:21:47 -04:00
parent 8015b0af74
commit d3a664fea4
No known key found for this signature in database
GPG key ID: FE0F173B8FC01571
59 changed files with 43 additions and 38 deletions

View file

@ -0,0 +1,4 @@
FROM ghcr.io/xtruder/nix-devcontainer:v1
# cache /nix
VOLUME /nix

View file

@ -0,0 +1,48 @@
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at
// https://github.com/microsoft/vscode-dev-containers/tree/master/containers/docker-existing-dockerfile
{
"name": "devcontainer-project",
"dockerFile": "Dockerfile",
"context": "${localWorkspaceFolder}",
"build": {
"args": {
"USER_UID": "${localEnv:USER_UID}",
"USER_GID": "${localEnv:USER_GID}"
}
},
// run arguments passed to docker
"runArgs": [
"--security-opt", "label=disable"
],
"containerEnv": {
// extensions to preload before other extensions
"PRELOAD_EXTENSIONS": "arrterian.nix-env-selector"
},
// disable command overriding and updating remote user ID
"overrideCommand": false,
"userEnvProbe": "loginShell",
"updateRemoteUserUID": false,
// build development environment on creation, make sure you already have shell.nix
"onCreateCommand": "nix-shell --command 'echo done building nix dev environment'",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
3000
],
"customizations": {
"vscode": {
"extensions": [
"arrterian.nix-env-selector"
]
}
}
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "go version",
}

135
api/.gitignore vendored Normal file
View file

@ -0,0 +1,135 @@
# Output
out
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.tmp

16
api/Dockerfile Normal file
View file

@ -0,0 +1,16 @@
FROM node:20.11.1-bookworm
WORKDIR /app/
COPY src /app/src
COPY prisma /app/prisma
COPY docker-entrypoint.sh /app/
COPY tsconfig.json /app/
COPY package.json /app/
COPY package-lock.json /app/
COPY srcpatch.sh /app/
RUN sh srcpatch.sh
RUN npm install --save-dev
RUN npm run build
RUN rm srcpatch.sh out/**/*.ts out/**/*.map
RUN rm -rf src
RUN npm prune --production
ENTRYPOINT sh docker-entrypoint.sh

7
api/dev.env Normal file
View file

@ -0,0 +1,7 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://nextnet:nextnet@localhost:5432/nextnet?schema=nextnet"

6
api/docker-entrypoint.sh Normal file
View file

@ -0,0 +1,6 @@
#!/bin/bash
echo "Welcome to NextNet."
echo "Running database migrations..."
npx prisma migrate deploy
echo "Starting application..."
npm start

32
api/init.sh Executable file
View file

@ -0,0 +1,32 @@
if [ ! -d ".tmp" ]; then
echo "Hello and welcome to the NextNet (backend) project! Please wait while I initialize things for you..."
cp dev.env .env
mkdir .tmp
fi
lsof -i:5432 | grep postgres 2> /dev/null > /dev/null
IS_PG_RUNNING=$?
if [ ! -f ".tmp/ispginit" ]; then
if [[ "$IS_PG_RUNNING" == 0 ]]; then
kill -9 $(lsof -t -i:5432) > /dev/null 2> /dev/null
fi
echo " - Database not initialized! Initializing database..."
mkdir .tmp/pglock
initdb -D .tmp/db
pg_ctl -D .tmp/db -l .tmp/logfile -o "--unix_socket_directories='$PWD/.tmp/pglock/'" start
createdb -h localhost -p 5432 nextnet
psql -h localhost -p 5432 nextnet -c "CREATE ROLE nextnet WITH LOGIN SUPERUSER PASSWORD 'nextnet';"
npm install --save-dev
npx prisma migrate dev
touch .tmp/ispginit
elif [[ "$IS_PG_RUNNING" == 1 ]]; then
pg_ctl -D .tmp/db -l .tmp/logfile -o "--unix_socket_directories='$PWD/.tmp/pglock/'" start
fi
source .env # Make sure we actually load correctly

1696
api/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

30
api/package.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "nextnet",
"version": "0.1.0",
"description": "Yet another dashboard to manage portforwarding technologies",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"start": "cd out && node --enable-source-maps index.js",
"dev": "nodemon --watch src --ext ts,js,mjs,json --exec \"tsc && cd out && node --enable-source-maps index.js\""
},
"keywords": [],
"author": "greysoh",
"license": "BSD-3-Clause",
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/node": "^20.12.7",
"@types/ssh2": "^1.15.0",
"nodemon": "^3.0.3",
"prisma": "^5.13.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@prisma/client": "^5.13.0",
"bcrypt": "^5.1.1",
"fastify": "^4.26.2",
"node-ssh": "^13.2.0"
}
}

View file

@ -0,0 +1,53 @@
-- CreateTable
CREATE TABLE "DesinationProvider" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"backend" TEXT NOT NULL,
"connectionDetails" TEXT NOT NULL,
CONSTRAINT "DesinationProvider_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ForwardRule" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"sourceIP" TEXT NOT NULL,
"sourcePort" INTEGER NOT NULL,
"destIP" TEXT NOT NULL,
"destPort" INTEGER NOT NULL,
"destProviderID" INTEGER NOT NULL,
"enabled" BOOLEAN NOT NULL,
CONSTRAINT "ForwardRule_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Permission" (
"id" SERIAL NOT NULL,
"permission" TEXT NOT NULL,
"has" BOOLEAN NOT NULL,
"userID" INTEGER NOT NULL,
CONSTRAINT "Permission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"password" TEXT NOT NULL,
"rootToken" TEXT,
"isRootServiceAccount" BOOLEAN,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Permission" ADD CONSTRAINT "Permission_userID_fkey" FOREIGN KEY ("userID") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View file

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

View file

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

View file

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

53
api/prisma/schema.prisma Normal file
View file

@ -0,0 +1,53 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model DesinationProvider {
id Int @id @default(autoincrement())
name String
description String?
backend String
connectionDetails String
}
model ForwardRule {
id Int @id @default(autoincrement())
name String
description String?
protocol String
sourceIP String
sourcePort Int
destPort Int
destProviderID Int
enabled Boolean
}
model Permission {
id Int @id @default(autoincrement())
permission String
has Boolean
user User @relation(fields: [userID], references: [id])
userID Int
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
password String // Will be hashed using bcrypt
rootToken String?
isRootServiceAccount Boolean?
permissions Permission[]
}

5
api/prod-docker.env Normal file
View file

@ -0,0 +1,5 @@
# These are default values, please change these!
POSTGRES_USERNAME=nextnet
POSTGRES_PASSWORD=nextnet
POSTGRES_DB=nextnet

View file

@ -0,0 +1,23 @@
meta {
name: Create
type: http
seq: 1
}
post {
url: http://127.0.0.1:3000/api/v1/backends/create
body: json
auth: none
}
body:json {
{
"token": "914abf2223f84375eed884671bfaefd7755d378af496b345f322214e75b51ed4465f11e26c944914c9b4fcc35c53250325fbc6530853ddfed8f72976d6fc5",
"name": "SSH Route",
"description": "This is a test route for SSH connectivity",
"backend": "ssh",
"connectionDetails": {
"funny": true
}
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Lookup
type: http
seq: 3
}
post {
url: http://127.0.0.1:3000/api/v1/backends/remove
body: json
auth: none
}
body:json {
{
"token": "f1b89cc337073476289ade17ffbe7a6419b4bd52aa7ede26114bffd76fa263b5cb1bcaf389462e1d9e7acb7f4b6a7c28152a9cc9af83e3ec862f1892b1",
"id": "2"
}
}

View file

@ -0,0 +1,23 @@
meta {
name: Remove
type: http
seq: 2
}
post {
url: http://127.0.0.1:3000/api/v1/backends/create
body: json
auth: none
}
body:json {
{
"token": "f1b89cc337073476289ade17ffbe7a6419b4bd52aa7ede26114bffd76fa263b5cb1bcaf389462e1d9e7acb7f4b6a7c28152a9cc9af83e3ec862f1892b1",
"name": "PortCopier Route",
"description": "This is a test route for portcopier.",
"backend": "PortCopier",
"connectionDetails": {
"funny": true
}
}
}

View file

@ -0,0 +1,28 @@
meta {
name: Create
type: http
seq: 1
}
post {
url: http://127.0.0.1:3000/api/v1/forward/create
body: json
auth: none
}
body:json {
{
"token": "914abf2223f84375eed884671bfaefd7755d378af496b345f322214e75b51ed4465f11e26c944914c9b4fcc35c53250325fbc6530853ddfed8f72976d6fc5",
"name": "Test Route",
"description": "This is a test route for SSH",
"protocol": "tcp",
"sourceIP": "127.0.0.1",
"sourcePort": "8000",
"destinationPort": "9000",
"providerID": "1"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Get Inbound Connections
type: http
seq: 6
}
post {
url: http://127.0.0.1:3000/api/v1/forward/connections
body: json
auth: none
}
body:json {
{
"token": "914abf2223f84375eed884671bfaefd7755d378af496b345f322214e75b51ed4465f11e26c944914c9b4fcc35c53250325fbc6530853ddfed8f72976d6fc5",
"id": "1"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Lookup
type: http
seq: 3
}
post {
url: http://127.0.0.1:3000/api/v1/forward/lookup
body: json
auth: none
}
body:json {
{
"token": "535c80825631c04b9add7a8682e06799d62ba57b5089b557f5bab2183fc9926b187b3b8d96da8ef16c67ec80f2917cf81bc21337f47728534f58ac9c4ed5f3fe",
"protocol": "tcp"
}
}

View file

@ -0,0 +1,26 @@
meta {
name: Remove
type: http
seq: 2
}
post {
url: http://127.0.0.1:3000/api/v1/forward/remove
body: json
auth: none
}
body:json {
{
"token": "f1b89cc337073476289ade17ffbe7a6419b4bd52aa7ede26114bffd76fa263b5cb1bcaf389462e1d9e7acb7f4b6a7c28152a9cc9af83e3ec862f1892b1",
"name": "Test Route",
"description": "This is a test route for portcopier.",
"sourceIP": "127.0.0.1",
"sourcePort": "8000",
"destinationPort": "9000",
"providerID": "1"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Start
type: http
seq: 4
}
post {
url: http://127.0.0.1:3000/api/v1/forward/start
body: json
auth: none
}
body:json {
{
"token": "914abf2223f84375eed884671bfaefd7755d378af496b345f322214e75b51ed4465f11e26c944914c9b4fcc35c53250325fbc6530853ddfed8f72976d6fc5",
"id": "1"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Stop
type: http
seq: 5
}
post {
url: http://127.0.0.1:3000/api/v1/forward/stop
body: json
auth: none
}
body:json {
{
"token": "914abf2223f84375eed884671bfaefd7755d378af496b345f322214e75b51ed4465f11e26c944914c9b4fcc35c53250325fbc6530853ddfed8f72976d6fc5",
"id": "1"
}
}

View file

@ -0,0 +1,17 @@
meta {
name: Get Permissions
type: http
seq: 1
}
post {
url: http://127.0.0.1:3000/api/v1/getPermissions
body: json
auth: none
}
body:json {
{
"token": "5e2cb92a338a832d385790861312eb85d69f46f82317bfa984ac5e3517368ab5a827897b0f9775a9181b02fa3b9cffed7e59e5b3111d5bdc37f729156caf5f"
}
}

View file

@ -0,0 +1,19 @@
meta {
name: Create
type: http
seq: 1
}
post {
url: http://127.0.0.1:3000/api/v1/users/create
body: json
auth: inherit
}
body:json {
{
"name": "Greysoh Hofuh",
"email": "greyson@hofers.cloud",
"password": "hunter123"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Log In
type: http
seq: 2
}
post {
url: http://127.0.0.1:3000/api/v1/users/login
body: json
auth: none
}
body:json {
{
"email": "me@greysoh.dev",
"password": "password"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Lookup
type: http
seq: 4
}
post {
url: http://127.0.0.1:3000/api/v1/users/lookup
body: json
auth: none
}
body:json {
{
"token": "5e2cb92a338a832d385790861312eb85d69f46f82317bfa984ac5e3517368ab5a827897b0f9775a9181b02fa3b9cffed7e59e5b3111d5bdc37f729156caf5f",
"name": "Greyson Hofer"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: Remove
type: http
seq: 3
}
post {
url: http://127.0.0.1:3000/api/v1/users/remove
body: json
auth: inherit
}
body:json {
{
"token": "5e2cb92a338a832d385790861312eb85d69f46f82317bfa984ac5e3517368ab5a827897b0f9775a9181b02fa3b9cffed7e59e5b3111d5bdc37f729156caf5f",
"uid": "2"
}
}

View file

@ -0,0 +1,5 @@
{
"version": "1",
"name": "NextNet API",
"type": "collection"
}

View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
npx @usebruno/cli run "$1" --output /tmp/out.json
cat /tmp/out.json | less

View file

@ -0,0 +1,59 @@
export type ParameterReturnedValue = {
success: boolean,
message?: string
}
export type ForwardRule = {
sourceIP: string,
sourcePort: number,
destPort: number
};
export type ConnectedClient = {
ip: string,
port: number,
connectionDetails: ForwardRule
};
export class BackendBaseClass {
state: "stopped" | "stopping" | "started" | "starting";
clients?: ConnectedClient[]; // Not required to be implemented, but more consistency
logs: string[];
constructor(parameters: string) {
this.logs = [];
this.clients = [];
this.state = "stopped";
}
addConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): void {};
removeConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): void {};
async start(): Promise<boolean> {
return true;
}
async stop(): Promise<boolean> {
return true;
};
getAllConnections(): ConnectedClient[] {
if (this.clients == null) return [];
return this.clients;
};
static checkParametersConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): ParameterReturnedValue {
return {
success: true
}
};
static checkParametersBackendInstance(data: string): ParameterReturnedValue {
return {
success: true
}
};
}

View file

@ -0,0 +1,6 @@
import type { BackendBaseClass } from "./base.js";
import { SSHBackendProvider } from "./ssh.js";
export const backendProviders: Record<string, typeof BackendBaseClass> = {
"ssh": SSHBackendProvider
};

215
api/src/backendimpl/ssh.ts Normal file
View file

@ -0,0 +1,215 @@
import { NodeSSH } from "node-ssh";
import { Socket } from "node:net";
import type { BackendBaseClass, ForwardRule, ConnectedClient, ParameterReturnedValue } from "./base.js";
type ForwardRuleExt = ForwardRule & {
enabled: boolean
}
// Fight me (for better naming)
type BackendParsedProviderString = {
ip: string,
port: number,
username: string,
privateKey: string
}
function parseBackendProviderString(data: string): BackendParsedProviderString {
try {
JSON.parse(data);
} catch (e) {
throw new Error("Payload body is not JSON")
}
const jsonData = JSON.parse(data);
if (typeof jsonData.ip != "string") throw new Error("IP field is not a string");
if (typeof jsonData.port != "number") throw new Error("Port is not a number");
if (typeof jsonData.username != "string") throw new Error("Username is not a string");
if (typeof jsonData.privateKey != "string") throw new Error("Private key is not a string");
return {
ip: jsonData.ip,
port: jsonData.port,
username: jsonData.username,
privateKey: jsonData.privateKey
}
}
export class SSHBackendProvider implements BackendBaseClass {
state: "stopped" | "stopping" | "started" | "starting";
clients: ConnectedClient[];
proxies: ForwardRuleExt[];
logs: string[];
sshInstance: NodeSSH;
options: BackendParsedProviderString;
constructor(parameters: string) {
this.logs = [];
this.proxies = [];
this.clients = [];
this.options = parseBackendProviderString(parameters);
this.state = "stopped";
}
async start(): Promise<boolean> {
this.state = "starting";
this.logs.push("Starting SSHBackendProvider...");
if (this.sshInstance) {
this.sshInstance.dispose();
}
this.sshInstance = new NodeSSH();
try {
await this.sshInstance.connect({
host: this.options.ip,
port: this.options.port,
username: this.options.username,
privateKey: this.options.privateKey
});
} catch (e) {
this.logs.push(`Failed to start SSHBackendProvider! Error: '${e}'`);
this.state = "stopped";
// @ts-ignore
this.sshInstance = null;
return false;
};
this.state = "started";
this.logs.push("Successfully started SSHBackendProvider.");
return true;
}
async stop(): Promise<boolean> {
this.state = "stopping";
this.logs.push("Stopping SSHBackendProvider...");
this.proxies.splice(0, this.proxies.length);
this.sshInstance.dispose();
// @ts-ignore
this.sshInstance = null;
this.logs.push("Successfully stopped SSHBackendProvider.");
this.state = "stopped";
return true;
};
addConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): void {
const connectionCheck = SSHBackendProvider.checkParametersConnection(sourceIP, sourcePort, destPort, protocol);
if (!connectionCheck.success) throw new Error(connectionCheck.message);
const foundProxyEntry = this.proxies.find((i) => i.sourceIP == sourceIP && i.sourcePort == sourcePort && i.destPort == destPort);
if (foundProxyEntry) return;
(async() => {
await this.sshInstance.forwardIn("0.0.0.0", destPort, (info, accept, reject) => {
const foundProxyEntry = this.proxies.find((i) => i.sourceIP == sourceIP && i.sourcePort == sourcePort && i.destPort == destPort);
if (!foundProxyEntry) return reject();
if (!foundProxyEntry.enabled) return reject();
const client: ConnectedClient = {
ip: info.srcIP,
port: info.srcPort,
connectionDetails: foundProxyEntry
};
this.clients.push(client);
const srcConn = new Socket();
srcConn.connect({
host: sourceIP,
port: sourcePort
});
// Why is this so confusing
const destConn = accept();
destConn.on("data", (chunk: Uint8Array) => {
srcConn.write(chunk);
});
destConn.on("exit", () => {
this.clients.splice(this.clients.indexOf(client), 1);
srcConn.end();
});
srcConn.on("data", (data) => {
destConn.write(data);
});
srcConn.on("end", () => {
this.clients.splice(this.clients.indexOf(client), 1);
destConn.close();
});
});
})();
this.proxies.push({
sourceIP,
sourcePort,
destPort,
enabled: true
});
};
removeConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): void {
const connectionCheck = SSHBackendProvider.checkParametersConnection(sourceIP, sourcePort, destPort, protocol);
if (!connectionCheck.success) throw new Error(connectionCheck.message);
const foundProxyEntry = this.proxies.find((i) => i.sourceIP == sourceIP && i.sourcePort == sourcePort && i.destPort == destPort);
if (!foundProxyEntry) return;
foundProxyEntry.enabled = false;
};
getAllConnections(): ConnectedClient[] {
return this.clients;
};
static checkParametersConnection(sourceIP: string, sourcePort: number, destPort: number, protocol: "tcp" | "udp"): ParameterReturnedValue {
if (protocol == "udp") return {
success: false,
message: "SSH does not support UDP tunneling! Please use something like PortCopier instead (if it gets done)"
};
return {
success: true
}
};
static checkParametersBackendInstance(data: string): ParameterReturnedValue {
try {
parseBackendProviderString(data);
// @ts-ignore
} catch (e: Error) {
return {
success: false,
message: e.toString()
}
}
return {
success: true
}
};
}

103
api/src/index.ts Normal file
View file

@ -0,0 +1,103 @@
import process from "node:process";
import { PrismaClient } from '@prisma/client';
import Fastify from "fastify";
import type { ServerOptions, SessionToken, RouteOptions } from "./libs/types.js";
import type { BackendBaseClass } from "./backendimpl/base.js";``
import { route as getPermissions } from "./routes/getPermissions.js";
import { route as backendCreate } from "./routes/backends/create.js";
import { route as backendRemove } from "./routes/backends/remove.js";
import { route as backendLookup } from "./routes/backends/lookup.js";
import { route as forwardConnections } from "./routes/forward/connections.js";
import { route as forwardCreate } from "./routes/forward/create.js";
import { route as forwardRemove } from "./routes/forward/remove.js";
import { route as forwardLookup } from "./routes/forward/lookup.js";
import { route as forwardStart } from "./routes/forward/start.js";
import { route as forwardStop } from "./routes/forward/stop.js";
import { route as userCreate } from "./routes/user/create.js";
import { route as userRemove } from "./routes/user/remove.js";
import { route as userLookup } from "./routes/user/lookup.js";
import { route as userLogin } from "./routes/user/login.js";
import { backendInit } from "./libs/backendInit.js";
const prisma = new PrismaClient();
const isSignupEnabled: boolean = Boolean(process.env.IS_SIGNUP_ENABLED);
const unsafeAdminSignup: boolean = Boolean(process.env.UNSAFE_ADMIN_SIGNUP);
const noUsersCheck: boolean = await prisma.user.count() == 0;
if (unsafeAdminSignup) {
console.error("WARNING: You have admin sign up on! This means that anyone that signs up will have admin rights!");
}
const serverOptions: ServerOptions = {
isSignupEnabled: isSignupEnabled ? true : noUsersCheck,
isSignupAsAdminEnabled: unsafeAdminSignup ? true : noUsersCheck,
allowUnsafeGlobalTokens: process.env.NODE_ENV != "production"
};
const sessionTokens: Record<number, SessionToken[]> = {};
const backends: Record<number, BackendBaseClass> = {};
const fastify = Fastify({
logger: true
});
const routeOptions: RouteOptions = {
fastify: fastify,
prisma: prisma,
tokens: sessionTokens,
options: serverOptions,
backends: backends
};
console.log("Initializing forwarding rules...");
const createdBackends = await prisma.desinationProvider.findMany();
for (const backend of createdBackends) {
console.log(`Running init steps for ID '${backend.id}' (${backend.name})`);
const init = await backendInit(backend, backends, prisma);
if (init) console.log("Init successful.");
}
console.log("Done.");
getPermissions(routeOptions);
backendCreate(routeOptions);
backendRemove(routeOptions);
backendLookup(routeOptions);
forwardConnections(routeOptions);
forwardCreate(routeOptions);
forwardRemove(routeOptions);
forwardLookup(routeOptions);
forwardStart(routeOptions);
forwardStop(routeOptions);
userCreate(routeOptions);
userRemove(routeOptions);
userLookup(routeOptions);
userLogin(routeOptions);
// Run the server!
try {
await fastify.listen({
port: 3000,
host: process.env.NODE_ENV == "production" ? "0.0.0.0" : "127.0.0.1"
});
} catch (err) {
fastify.log.error(err);
process.exit(1);
}

View file

@ -0,0 +1,53 @@
import type { PrismaClient } from "@prisma/client";
import type { BackendBaseClass } from "../backendimpl/base.js";
import { backendProviders } from "../backendimpl/index.js";
type Backend = {
id: number;
name: string;
description: string | null;
backend: string;
connectionDetails: string;
};
export async function backendInit(backend: Backend, backends: Record<number, BackendBaseClass>, prisma: PrismaClient): Promise<boolean> {
const ourProvider = backendProviders[backend.backend];
if (!ourProvider) {
console.log(" - Error: Invalid backend recieved!");
return false;
}
console.log(" - Initializing backend...");
backends[backend.id] = new ourProvider(backend.connectionDetails);
const ourBackend = backends[backend.id];
if (!await ourBackend.start()) {
console.log(" - Error initializing backend!");
console.log(" - " + ourBackend.logs.join("\n - "));
return false;
}
console.log(" - Initializing clients...");
const clients = await prisma.forwardRule.findMany({
where: {
destProviderID: backend.id,
enabled: true
}
});
for (const client of clients) {
if (client.protocol != "tcp" && client.protocol != "udp") {
console.error(` - Error: Client with ID of '${client.id}' has an invalid protocol! (must be either TCP or UDP)`);
continue;
}
ourBackend.addConnection(client.sourceIP, client.sourcePort, client.destPort, client.protocol);
}
return true;
}

View file

@ -0,0 +1,26 @@
function getRandomInt(min: number, max: number): number {
const minCeiled = Math.ceil(min);
const maxFloored = Math.floor(max);
return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
}
export function generateRandomData(length: number): string {
let newString = "";
for (let i = 0; i < length; i += 2) {
const randomNumber = getRandomInt(0, 255);
if (randomNumber == 0) {
i -= 2;
continue;
}
newString += randomNumber.toString(16);
}
return newString;
}
export function generateToken() {
return generateRandomData(128);
}

View file

@ -0,0 +1,92 @@
import type { PrismaClient } from "@prisma/client";
import type { SessionToken } from "./types.js";
export const permissionListDisabled: Record<string, boolean> = {
"routes.add": false,
"routes.remove": false,
"routes.start": false,
"routes.stop": false,
"routes.edit": false,
"routes.visible": false,
"routes.visibleConn": false,
"backends.add": false,
"backends.remove": false,
"backends.start": false,
"backends.stop": false,
"backends.edit": false,
"backends.visible": false,
"backends.secretVis": false,
"permissions.see": false,
"users.add": false,
"users.remove": false,
"users.lookup": false,
"users.edit": false,
};
// FIXME: This solution fucking sucks.
export let permissionListEnabled: Record<string, boolean> = JSON.parse(JSON.stringify(permissionListDisabled));
for (const index of Object.keys(permissionListEnabled)) {
permissionListEnabled[index] = true;
}
export async function hasPermission(permissionList: string[], uid: number, prisma: PrismaClient): Promise<boolean> {
for (const permission of permissionList) {
const permissionNode = await prisma.permission.findFirst({
where: {
userID: uid,
permission
}
});
if (!permissionNode || !permissionNode.has) return false;
}
return true;
}
export async function getUID(token: string, tokens: Record<number, SessionToken[]>, prisma: PrismaClient): Promise<number> {
let userID = -1;
// Look up in our currently authenticated users
for (const otherTokenKey of Object.keys(tokens)) {
const otherTokenList = tokens[parseInt(otherTokenKey)];
for (const otherTokenIndex in otherTokenList) {
const otherToken = otherTokenList[otherTokenIndex];
if (otherToken.token == token) {
if (otherToken.expiresAt < otherToken.createdAt + (otherToken.createdAt - Date.now())) {
otherTokenList.splice(parseInt(otherTokenIndex), 1);
continue;
} else {
userID = parseInt(otherTokenKey);
}
}
}
}
// Fine, we'll look up for global tokens...
// FIXME: Could this be more efficient? IDs are sequential in SQL I think
if (userID == -1) {
const allUsers = await prisma.user.findMany({
where: {
isRootServiceAccount: true
}
});
for (const user of allUsers) {
if (user.rootToken == token) userID = user.id;
};
}
return userID;
}
export async function hasPermissionByToken(permissionList: string[], token: string, tokens: Record<number, SessionToken[]>, prisma: PrismaClient): Promise<boolean> {
const userID = await getUID(token, tokens, prisma);
return await hasPermission(permissionList, userID, prisma);
}

28
api/src/libs/types.ts Normal file
View file

@ -0,0 +1,28 @@
import type { PrismaClient } from "@prisma/client";
import type { FastifyInstance } from "fastify";
import type { BackendBaseClass } from "../backendimpl/base.js";
export type ServerOptions = {
isSignupEnabled: boolean;
isSignupAsAdminEnabled: boolean;
allowUnsafeGlobalTokens: boolean;
}
// NOTE: Someone should probably use Redis for this, but this is fine...
export type SessionToken = {
createdAt: number,
expiresAt: number, // Should be (createdAt + (30 minutes))
token: string
};
export type RouteOptions = {
fastify: FastifyInstance,
prisma: PrismaClient,
tokens: Record<number, SessionToken[]>,
options: ServerOptions,
backends: Record<number, BackendBaseClass>
};

View file

@ -0,0 +1,17 @@
# Route Plan
- [x] /api/v1/users/create
- [x] /api/v1/users/login
- [x] /api/v1/users/remove
- [ ] /api/v1/users/modify
- [x] /api/v1/users/lookup
- [x] /api/v1/backends/create
- [x] /api/v1/backends/remove
- [ ] /api/v1/backends/modify
- [x] /api/v1/backends/lookup
- [x] /api/v1/routes/create
- [x] /api/v1/routes/remove
- [ ] /api/v1/routes/modify
- [x] /api/v1/routes/lookup
- [ ] /api/v1/routes/start
- [ ] /api/v1/routes/stop
- [x] /api/v1/getPermissions

View file

@ -0,0 +1,94 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
import { backendProviders } from "../../backendimpl/index.js";
import { backendInit } from "../../libs/backendInit.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens,
backends
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
};
/**
* Creates a new backend to use
*/
fastify.post("/api/v1/backends/create", {
schema: {
body: {
type: "object",
required: ["token", "name", "backend", "connectionDetails"],
properties: {
token: { type: "string" },
name: { type: "string" },
description: { type: "string" },
backend: { type: "string" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
name: string,
description?: string,
connectionDetails: any,
backend: string
} = req.body;
if (!await hasPermission(body.token, [
"backends.add"
])) {
return res.status(403).send({
error: "Unauthorized"
});
};
if (!backendProviders[body.backend]) {
return res.status(400).send({
error: "Unknown/unsupported/deprecated backend!"
});
};
const connectionDetails = JSON.stringify(body.connectionDetails);
const connectionDetailsValidityCheck = backendProviders[body.backend].checkParametersBackendInstance(connectionDetails);
if (!connectionDetailsValidityCheck.success) {
return res.status(400).send({
error: connectionDetailsValidityCheck.message ?? "Unknown error while attempting to parse connectionDetails (it's on your side)"
});
};
const backend = await prisma.desinationProvider.create({
data: {
name: body.name,
description: body.description,
backend: body.backend,
connectionDetails: JSON.stringify(body.connectionDetails)
}
});
const init = await backendInit(backend, backends, prisma);
if (!init) {
// TODO: better error code
return res.status(504).send({
error: "Backend is created, but failed to initalize correctly",
id: backend.id
});
}
return {
success: true,
id: backend.id
};
});
}

View file

@ -0,0 +1,78 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens,
backends
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
};
/**
* Creates a new route to use
*/
fastify.post("/api/v1/backends/lookup", {
schema: {
body: {
type: "object",
required: ["token"],
properties: {
token: { type: "string" },
id: { type: "number" },
name: { type: "string" },
description: { type: "string" },
backend: { type: "string" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
id?: number,
name?: string,
description?: string,
backend?: string
} = req.body;
if (!await hasPermission(body.token, [
"backends.visible" // wtf?
])) {
return res.status(403).send({
error: "Unauthorized"
});
};
const canSeeSecrets = await hasPermission(body.token, [
"backends.secretVis"
]);
const prismaBackends = await prisma.desinationProvider.findMany({
where: {
id: body.id,
name: body.name,
description: body.description,
backend: body.backend
}
});
return {
success: true,
data: prismaBackends.map((i) => ({
name: i.name,
description: i.description,
backend: i.backend,
connectionDetails: canSeeSecrets ? i.connectionDetails : "",
logs: backends[i.id].logs
}))
}
});
}

View file

@ -0,0 +1,71 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens,
backends
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
};
/**
* Creates a new route to use
*/
fastify.post("/api/v1/backends/remove", {
schema: {
body: {
type: "object",
required: ["token", "id"],
properties: {
token: { type: "string" },
id: { type: "number" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
id: number
} = req.body;
if (!await hasPermission(body.token, [
"backends.remove"
])) {
return res.status(403).send({
error: "Unauthorized"
});
};
if (!backends[body.id]) {
return res.status(400).send({
error: "Backend not found"
});
};
// Unload the backend
if (!await backends[body.id].stop()) {
return res.status(400).send({
error: "Failed to stop backend! Please report this issue."
})
}
delete backends[body.id];
await prisma.desinationProvider.delete({
where: {
id: body.id
}
});
return {
success: true
}
});
}

View file

@ -0,0 +1,62 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens,
backends
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
};
fastify.post("/api/v1/forward/connections", {
schema: {
body: {
type: "object",
required: ["token", "id"],
properties: {
token: { type: "string" },
id: { type: "number" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
id: number
} = req.body;
if (!await hasPermission(body.token, [
"routes.visibleConn"
])) {
return res.status(403).send({
error: "Unauthorized"
});
};
const forward = await prisma.forwardRule.findUnique({
where: {
id: body.id
}
});
if (!forward) return res.status(400).send({
error: "Could not find forward entry"
});
if (!backends[forward.destProviderID]) return res.status(400).send({
error: "Backend not found"
});
return {
success: true,
data: backends[forward.destProviderID].getAllConnections()
}
})
}

View file

@ -0,0 +1,109 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
};
/**
* Creates a new route to use
*/
fastify.post("/api/v1/forward/create", {
schema: {
body: {
type: "object",
required: ["token", "name", "protocol", "sourceIP", "sourcePort", "destinationPort", "providerID"],
properties: {
token: { type: "string" },
name: { type: "string" },
description: { type: "string" },
protocol: { type: "string" },
sourceIP: { type: "string" },
sourcePort: { type: "number" },
destinationPort: { type: "number" },
providerID: { type: "number" },
autoStart: { type: "boolean" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
name: string,
description?: string,
protocol: "tcp" | "udp",
sourceIP: string,
sourcePort: number,
destinationPort: number,
providerID: number,
autoStart?: boolean
} = req.body;
if (body.protocol != "tcp" && body.protocol != "udp") {
return res.status(400).send({
error: "Body protocol field must be either tcp or udp"
});
};
if (!await hasPermission(body.token, [
"routes.add"
])) {
return res.status(403).send({
error: "Unauthorized"
});
};
const lookupIDForDestProvider = await prisma.desinationProvider.findUnique({
where: {
id: body.providerID
}
});
if (!lookupIDForDestProvider) return res.status(400).send({
error: "Could not find provider"
});
const forwardRule = await prisma.forwardRule.create({
data: {
name: body.name,
description: body.description,
protocol: body.protocol,
sourceIP: body.sourceIP,
sourcePort: body.sourcePort,
destPort: body.destinationPort,
destProviderID: body.providerID,
enabled: Boolean(body.autoStart)
}
});
return {
success: true,
id: forwardRule.id
}
});
}

View file

@ -0,0 +1,108 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
};
/**
* Creates a new route to use
*/
fastify.post("/api/v1/forward/lookup", {
schema: {
body: {
type: "object",
required: ["token"],
properties: {
token: { type: "string" },
id: { type: "number" },
name: { type: "string" },
protocol: { type: "string" },
description: { type: "string" },
sourceIP: { type: "string" },
sourcePort: { type: "number" },
destPort: { type: "number" },
providerID: { type: "number" },
autoStart: { type: "boolean" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
id?: number,
name?: string,
description?: string,
protocol?: "tcp" | "udp",
sourceIP?: string,
sourcePort?: number,
destinationPort?: number,
providerID?: number,
autoStart?: boolean
} = req.body;
if (body.protocol && body.protocol != "tcp" && body.protocol != "udp") {
return res.status(400).send({
error: "Protocol specified in body must be either 'tcp' or 'udp'"
})
}
if (!await hasPermission(body.token, [
"routes.visible" // wtf?
])) {
return res.status(403).send({
error: "Unauthorized"
});
};
const forwardRules = await prisma.forwardRule.findMany({
where: {
id: body.id,
name: body.name,
description: body.description,
sourceIP: body.sourceIP,
sourcePort: body.sourcePort,
destPort: body.destinationPort,
destProviderID: body.providerID,
enabled: body.autoStart
}
});
return {
success: true,
data: forwardRules.map((i) => ({
id: i.id,
name: i.name,
description: i.description,
sourceIP: i.sourceIP,
sourcePort: i.sourcePort,
destPort: i.destPort,
providerID: i.destProviderID,
autoStart: i.enabled // TODO: Add enabled flag in here to see if we're running or not
}))
};
});
}

View file

@ -0,0 +1,55 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
};
/**
* Creates a new route to use
*/
fastify.post("/api/v1/forward/remove", {
schema: {
body: {
type: "object",
required: ["token", "id"],
properties: {
token: { type: "string" },
id: { type: "number" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
id: number
} = req.body;
if (!await hasPermission(body.token, [
"routes.remove"
])) {
return res.status(403).send({
error: "Unauthorized"
});
};
await prisma.forwardRule.delete({
where: {
id: body.id
}
});
return {
success: true
}
});
}

View file

@ -0,0 +1,70 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens,
backends
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
};
/**
* Creates a new route to use
*/
fastify.post("/api/v1/forward/start", {
schema: {
body: {
type: "object",
required: ["token", "id"],
properties: {
token: { type: "string" },
id: { type: "number" }
}
},
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
id: number
} = req.body;
if (!await hasPermission(body.token, [
"routes.start"
])) {
return res.status(403).send({
error: "Unauthorized"
});
};
const forward = await prisma.forwardRule.findUnique({
where: {
id: body.id
}
});
if (!forward) return res.status(400).send({
error: "Could not find forward entry"
});
if (!backends[forward.destProviderID]) return res.status(400).send({
error: "Backend not found"
});
// Other restrictions in place make it so that it MUST be either TCP or UDP
// @ts-ignore
const protocol: "tcp" | "udp" = forward.protocol;
backends[forward.destProviderID].addConnection(forward.sourceIP, forward.sourcePort, forward.destPort, protocol);
return {
success: true
}
});
}

View file

@ -0,0 +1,70 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens,
backends
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
};
/**
* Creates a new route to use
*/
fastify.post("/api/v1/forward/stop", {
schema: {
body: {
type: "object",
required: ["token", "id"],
properties: {
token: { type: "string" },
id: { type: "number" }
}
},
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
id: number
} = req.body;
if (!await hasPermission(body.token, [
"routes.stop"
])) {
return res.status(403).send({
error: "Unauthorized"
});
};
const forward = await prisma.forwardRule.findUnique({
where: {
id: body.id
}
});
if (!forward) return res.status(400).send({
error: "Could not find forward entry"
});
if (!backends[forward.destProviderID]) return res.status(400).send({
error: "Backend not found"
});
// Other restrictions in place make it so that it MUST be either TCP or UDP
// @ts-ignore
const protocol: "tcp" | "udp" = forward.protocol;
backends[forward.destProviderID].removeConnection(forward.sourceIP, forward.sourcePort, forward.destPort, protocol);
return {
success: true
}
});
}

View file

@ -0,0 +1,53 @@
import { hasPermission, getUID } from "../libs/permissions.js";
import type { RouteOptions } from "../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens
} = routeOptions;
/**
* Logs in to a user account.
*/
fastify.post("/api/v1/getPermissions", {
schema: {
body: {
type: "object",
required: ["token"],
properties: {
token: { type: "string" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string
} = req.body;
const uid = await getUID(body.token, tokens, prisma);
if (!await hasPermission([
"permissions.see"
], uid, prisma)) {
return res.status(403).send({
error: "Unauthorized"
});
};
const permissionsRaw = await prisma.permission.findMany({
where: {
userID: uid
}
});
return {
success: true,
// Get the ones that we have, and transform them into just their name
data: permissionsRaw.filter((i) => i.has).map((i) => i.permission)
}
});
}

View file

@ -0,0 +1,118 @@
import { hash } from "bcrypt";
import { permissionListEnabled } from "../../libs/permissions.js";
import { generateToken } from "../../libs/generateToken.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens,
options
} = routeOptions;
/**
* Creates a new user account to use, only if it is enabled.
*/
fastify.post("/api/v1/users/create", {
schema: {
body: {
type: "object",
required: ["name", "email", "password"],
properties: {
name: { type: "string" },
email: { type: "string" },
password: { type: "string" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
name: string,
email: string,
password: string
} = req.body;
if (!options.isSignupEnabled) {
return res.status(403).send({
error: "Signing up is not enabled at this time."
});
};
const userSearch = await prisma.user.findFirst({
where: {
email: body.email
}
});
if (userSearch) {
return res.status(400).send({
error: "User already exists"
})
};
const saltedPassword: string = await hash(body.password, 15);
const userData = {
name: body.name,
email: body.email,
password: saltedPassword,
permissions: {
create: [] as {
permission: string,
has: boolean
}[]
}
};
// TODO: There's probably a faster way to pull this off, but I'm lazy
for (const permissionKey of Object.keys(permissionListEnabled)) {
if (options.isSignupAsAdminEnabled || (permissionKey.startsWith("routes") || permissionKey == "permissions.see")) {
userData.permissions.create.push({
permission: permissionKey,
has: permissionListEnabled[permissionKey]
});
}
};
if (options.allowUnsafeGlobalTokens) {
// @ts-ignore
userData.rootToken = generateToken();
// @ts-ignore
userData.isRootServiceAccount = true;
}
const userCreateResults = await prisma.user.create({
data: userData
});
// FIXME(?): Redundant checks
if (options.allowUnsafeGlobalTokens) {
return {
success: true,
token: userCreateResults.rootToken
};
} else {
const generatedToken = generateToken();
tokens[userCreateResults.id] = [];
tokens[userCreateResults.id].push({
createdAt: Date.now(),
expiresAt: Date.now() + (30 * 60_000),
token: generatedToken
});
return {
success: true,
token: generatedToken
};
};
});
}

View file

@ -0,0 +1,66 @@
import { compare } from "bcrypt";
import { generateToken } from "../../libs/generateToken.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens
} = routeOptions;
/**
* Logs in to a user account.
*/
fastify.post("/api/v1/users/login", {
schema: {
body: {
type: "object",
required: ["email", "password"],
properties: {
email: { type: "string" },
password: { type: "string" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
email: string,
password: string
} = req.body;
const userSearch = await prisma.user.findFirst({
where: {
email: body.email
}
});
if (!userSearch) return res.status(403).send({
error: "Email or password is incorrect"
});
const passwordIsValid = await compare(body.password, userSearch.password);
if (!passwordIsValid) return res.status(403).send({
error: "Email or password is incorrect"
});
const token = generateToken();
if (!tokens[userSearch.id]) tokens[userSearch.id] = [];
tokens[userSearch.id].push({
createdAt: Date.now(),
expiresAt: Date.now() + (30 * 60_000),
token
});
return {
success: true,
token
}
});
}

View file

@ -0,0 +1,66 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
};
fastify.post("/api/v1/users/lookup", {
schema: {
body: {
type: "object",
required: ["token"],
properties: {
token: { type: "string" },
id: { type: "number" },
name: { type: "string" },
email: { type: "string" },
isServiceAccount: { type: "boolean" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
id?: number,
name?: string,
email?: string,
isServiceAccount?: boolean
} = req.body;
if (!await hasPermission(body.token, [
"users.lookup"
])) {
return res.status(403).send({
error: "Unauthorized"
});
};
const users = await prisma.user.findMany({
where: {
id: body.id,
name: body.name,
email: body.email,
isRootServiceAccount: body.isServiceAccount
}
});
return {
success: true,
data: users.map((i) => ({
name: i.name,
email: i.email,
isServiceAccount: i.isRootServiceAccount
}))
}
});
}

View file

@ -0,0 +1,61 @@
import { hasPermissionByToken } from "../../libs/permissions.js";
import type { RouteOptions } from "../../libs/types.js";
export function route(routeOptions: RouteOptions) {
const {
fastify,
prisma,
tokens
} = routeOptions;
function hasPermission(token: string, permissionList: string[]): Promise<boolean> {
return hasPermissionByToken(permissionList, token, tokens, prisma);
};
/**
* Creates a new backend to use
*/
fastify.post("/api/v1/users/remove", {
schema: {
body: {
type: "object",
required: ["token", "uid"],
properties: {
token: { type: "string" },
uid: { type: "number" }
}
}
}
}, async(req, res) => {
// @ts-ignore
const body: {
token: string,
uid: number
} = req.body;
if (!await hasPermission(body.token, [
"users.remove"
])) {
return res.status(403).send({
error: "Unauthorized"
});
};
await prisma.permission.deleteMany({
where: {
userID: body.uid
}
});
await prisma.user.delete({
where: {
id: body.uid
}
});
return {
success: true
}
});
};

6
api/srcpatch.sh Executable file
View file

@ -0,0 +1,6 @@
# !-- DO NOT USE THIS FOR DEVELOPMENT --!
# This is only to source patch files in production deployments, if prisma isn't configured already.
printf "//@ts-nocheck\n$(cat src/routes/backends/lookup.ts)" > src/routes/backends/lookup.ts
printf "//@ts-nocheck\n$(cat src/routes/forward/lookup.ts)" > src/routes/forward/lookup.ts
printf "//@ts-nocheck\n$(cat src/routes/user/lookup.ts)" > src/routes/user/lookup.ts
printf "//@ts-nocheck\n$(cat src/routes/getPermissions.ts)" > src/routes/getPermissions.ts

22
api/tsconfig.json Normal file
View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es2020",
"module": "es2022",
"moduleResolution": "node",
"outDir": "./out",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"strictPropertyInitialization": false,
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}