feature: Adds user creation endpoint.
This commit is contained in:
parent
6d0fc93138
commit
35e23357ef
10 changed files with 197 additions and 4 deletions
10
package-lock.json
generated
10
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
|
||||
|
|
11
routes/NextNet API/Create User.bru
Normal file
11
routes/NextNet API/Create User.bru
Normal 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
|
||||
}
|
5
routes/NextNet API/bruno.json
Normal file
5
routes/NextNet API/bruno.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"version": "1",
|
||||
"name": "NextNet API",
|
||||
"type": "collection"
|
||||
}
|
25
src/index.ts
25
src/index.ts
|
@ -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
20
src/libs/generateToken.ts
Normal 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
14
src/libs/types.ts
Normal 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
92
src/routes/user/create.ts
Normal 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
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue