feature: Adds user creation endpoint.

This commit is contained in:
greysoh 2024-04-17 15:39:29 -04:00
parent 6d0fc93138
commit 35e23357ef
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
10 changed files with 197 additions and 4 deletions

View file

@ -3,15 +3,34 @@ import process from "node:process";
import { PrismaClient } from '@prisma/client';
import Fastify from "fastify";
import { ServerOptions, SessionToken } from "./libs/types.js";
import { route as create } from "./routes/user/create.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 fastify = Fastify({
logger: true
});
fastify.get('/', async function handler (request, reply) {
return { hello: 'world' };
})
create(fastify, prisma, sessionTokens, serverOptions);
// Run the server!
try {

20
src/libs/generateToken.ts Normal file
View file

@ -0,0 +1,20 @@
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);
newString += randomNumber.toString(16);
}
return newString;
}
export function generateToken() {
return generateRandomData(128);
}

14
src/libs/types.ts Normal file
View file

@ -0,0 +1,14 @@
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
};

92
src/routes/user/create.ts Normal file
View file

@ -0,0 +1,92 @@
import type { PrismaClient } from "@prisma/client";
import type { FastifyInstance } from "fastify";
import { hash } from "bcrypt";
import { ServerOptions, SessionToken } from "../../libs/types.js";
import { generateToken } from "../../libs/generateToken.js";
export function route(fastify: FastifyInstance, prisma: PrismaClient, tokens: Record<number, SessionToken[]>, options: ServerOptions) {
// TODO: Permissions
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(400).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);
let userData = {
name: body.name,
email: body.email,
password: saltedPassword,
rootToken: null
};
if (options.allowUnsafeGlobalTokens) {
userData.rootToken = generateToken() as unknown as null;
}
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
};
};
});
}