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

10
package-lock.json generated
View file

@ -14,6 +14,7 @@
"fastify": "^4.26.2"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/node": "^20.12.7",
"nodemon": "^3.0.3",
"prisma": "^5.12.1",
@ -146,6 +147,15 @@
"@prisma/debug": "5.12.1"
}
},
"node_modules/@types/bcrypt": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
"integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "20.12.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",

View file

@ -14,6 +14,7 @@
"author": "greysoh",
"license": "BSD-3-Clause",
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/node": "^20.12.7",
"nodemon": "^3.0.3",
"prisma": "^5.12.1",

View file

@ -0,0 +1,21 @@
/*
Warnings:
- Made the column `name` on table `User` required. This step will fail if there are existing NULL values in that column.
*/
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"password" TEXT NOT NULL,
"rootToken" TEXT
);
INSERT INTO "new_User" ("email", "id", "name", "password", "rootToken") SELECT "email", "id", "name", "password", "rootToken" FROM "User";
DROP TABLE "User";
ALTER TABLE "new_User" RENAME TO "User";
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View file

@ -48,7 +48,7 @@ model User {
id Int @id @default(autoincrement())
email String @unique
name String?
name String
password String // Will be hashed using bcrypt

View file

@ -0,0 +1,11 @@
meta {
name: Create User
type: http
seq: 1
}
post {
url: http://127.0.0.1:3000/api/v1/users/create
body: none
auth: none
}

View file

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

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