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"
|
"fastify": "^4.26.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
"nodemon": "^3.0.3",
|
"nodemon": "^3.0.3",
|
||||||
"prisma": "^5.12.1",
|
"prisma": "^5.12.1",
|
||||||
|
@ -146,6 +147,15 @@
|
||||||
"@prisma/debug": "5.12.1"
|
"@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": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.12.7",
|
"version": "20.12.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"author": "greysoh",
|
"author": "greysoh",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
"nodemon": "^3.0.3",
|
"nodemon": "^3.0.3",
|
||||||
"prisma": "^5.12.1",
|
"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())
|
id Int @id @default(autoincrement())
|
||||||
|
|
||||||
email String @unique
|
email String @unique
|
||||||
name String?
|
name String
|
||||||
|
|
||||||
password String // Will be hashed using bcrypt
|
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 { PrismaClient } from '@prisma/client';
|
||||||
import Fastify from "fastify";
|
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 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({
|
const fastify = Fastify({
|
||||||
logger: true
|
logger: true
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get('/', async function handler (request, reply) {
|
create(fastify, prisma, sessionTokens, serverOptions);
|
||||||
return { hello: 'world' };
|
|
||||||
})
|
|
||||||
|
|
||||||
// Run the server!
|
// Run the server!
|
||||||
try {
|
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