This commit is contained in:
2026-09-06 20:54:19 -04:00
parent 7e285ffadd
commit f3a5d1799c
27 changed files with 3686 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules
*.db
+1
View File
@@ -0,0 +1 @@
.env
+35
View File
@@ -0,0 +1,35 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "tsx src/index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"devDependencies": {
"@types/bcrypt": "^6.0.0",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/express-session": "^1.19.0",
"@types/node": "^26.4.1",
"@types/passport": "^1.0.17",
"@types/passport-local": "^1.0.38",
"tsx": "^4.23.13",
"typescript": "^7.0.2"
},
"dependencies": {
"bcrypt": "^6.0.0",
"cors": "^2.8.6",
"database": "workspace:*",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-session": "^1.19.0",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"zod": "^4.5.4"
}
}
+82
View File
@@ -0,0 +1,82 @@
import type { Request, Response, NextFunction } from "express";
import { prisma } from "database";
import schemas from "../utils/schemas.js";
export async function createPost(
req: Request,
res: Response,
next: NextFunction,
) {
try {
const body = schemas.posts.create.parse(req.body);
const post = await prisma.post.create({
data: {
...body,
userId: req.user!.id,
},
});
res.status(201).json(post);
} catch (e) {
next(e);
}
}
export async function fetchPost(
req: Request,
res: Response,
next: NextFunction,
) {
try {
const slug = schemas.posts.get.parse(req.params.slug);
const post = await prisma.post.findFirst({
where: {
slug,
},
});
if (post) {
res.status(200).json(post);
return;
} else {
res.status(404).json({ message: "Could not find post with that slug" });
return;
}
} catch (e) {
next(e);
}
}
export async function getAllPosts(
req: Request,
res: Response,
next: NextFunction,
) {
try {
const posts = await prisma.post.findMany();
res.status(200).json(posts);
} catch (e) {
next(e);
}
}
export async function deletePost(
req: Request,
res: Response,
next: NextFunction,
) {
try {
const slug = schemas.posts.delete.parse(req.params.slug);
await prisma.post.findFirst({
where: {
slug,
},
});
res.sendStatus(204);
} catch (e) {
next(e);
}
}
+74
View File
@@ -0,0 +1,74 @@
import type { Request, Response, NextFunction } from "express";
import bcrypt from "bcrypt";
import schemas from "../utils/schemas.js";
import passport from "passport";
import { prisma } from "database";
export async function register(
req: Request,
res: Response,
next: NextFunction,
) {
try {
const body = schemas.users.registration.parse(req.body);
const email = body.email;
const passwordHash = await bcrypt.hash(body.password, 12);
const user = await prisma.user.create({
data: {
email,
password: passwordHash,
name: body.name,
},
select: {
id: true,
email: true,
name: true,
},
});
req.login(user, (err) => {
if (err) return next(err);
res.status(201).json({ user });
});
} catch (e) {
next(e);
}
}
export async function login(req: Request, res: Response, next: NextFunction) {
try {
passport.authenticate(
"local",
(
err: unknown,
user: Express.User | false,
info?: { message?: string },
) => {
if (err) return next(err);
if (!user) {
return res.status(401).json({
error: info?.message ?? "Incorrect email or password",
});
}
req.login(user, (loginErr) => {
if (loginErr) return next(loginErr);
res.json({ id: user.id, email: user.email, name: user.name });
});
},
)(req, res, next);
} catch (e) {
next(e);
}
}
export async function getUser(req: Request, res: Response, next: NextFunction) {
const user = req.user;
res.status(200).json({
id: user?.id,
email: user?.email,
name: user?.name,
});
}
+105
View File
@@ -0,0 +1,105 @@
import express, {
type Request,
type Response,
type NextFunction,
} from "express";
import cors from "cors";
import session from "express-session";
import passport from "passport";
import bcrypt from "bcrypt";
import env from "./utils/env.js";
import { Strategy as LocalStrategy } from "passport-local";
import { prisma } from "database";
import { ZodError } from "zod";
// routes
import usersRoute from "./routers/users.js";
import postsRoute from "./routers/posts.js";
const app = express();
// passport strategy
passport.use(
new LocalStrategy(
{ usernameField: "email" },
async (email, password, done) => {
try {
const user = await prisma.user.findFirst({
where: {
email,
},
});
if (!user) return done(null, false, { message: "Bad credentials" });
const ok = await bcrypt.compare(password, user.password);
return ok
? done(null, user)
: done(null, false, { message: "Bad credentials" });
} catch (err) {
return done(err);
}
},
),
);
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id: number, done) => {
const user = await prisma.user.findFirst({
where: {
id,
},
});
done(null, user ?? false);
});
app.use(express.urlencoded({ extended: false }));
app.use(
session({
secret: env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
}),
);
app.use(passport.session());
app.use(express.json());
if (env.NODE_ENV === "production") {
app.use(
cors({
origin: "blog.alexav.gg",
credentials: true,
}),
);
}
app.get("/", (req: Request, res: Response) => {
res.status(200).json({
status: 200,
});
});
app.use("/users", usersRoute);
app.use("/posts", postsRoute);
app.use(
(err: Error | ZodError, req: Request, res: Response, next: NextFunction) => {
if (err instanceof ZodError) {
res.status(400).json({
status: 400,
message: "Malformed request body",
issues: err.issues,
});
} else {
console.log(err);
res.status(500).json({
status: 500,
message:
"Sorry, an unexpected error occurred while performing that action",
});
}
},
);
app.listen(3000, () => {
console.log("Server started");
});
+17
View File
@@ -0,0 +1,17 @@
import { Router } from "express";
import {
createPost,
fetchPost,
getAllPosts,
deletePost,
} from "../controllers/posts.js";
import { requireAuth } from "../utils/auth.js";
const router: Router = Router();
router.post("/create", requireAuth, createPost);
router.get("/:slug", fetchPost);
router.get("/", getAllPosts);
router.delete("/:slug", requireAuth, deletePost);
export default router;
+14
View File
@@ -0,0 +1,14 @@
import { Router } from "express";
import { requireAuth } from "../utils/auth.js";
import { register, login, getUser } from "../controllers/users.js";
const router: Router = Router();
router.post("/login", login);
router.post("/logout", (req, res, next) => {
req.logout((err) => (err ? next(err) : res.redirect("/")));
});
router.post("/register", register);
router.get("/@me", requireAuth, getUser);
export default router;
+13
View File
@@ -0,0 +1,13 @@
import { User as PrismaUser } from "database";
declare global {
namespace Express {
interface User {
id: number;
name: string;
email: string;
}
}
}
export {};
+6
View File
@@ -0,0 +1,6 @@
import type { Request, Response, NextFunction } from "express";
export function requireAuth(req: Request, res: Response, next: NextFunction) {
if (req.isAuthenticated()) return next();
res.status(401).json({ error: "Not logged in" });
}
+11
View File
@@ -0,0 +1,11 @@
import "dotenv/config";
import { z } from "zod";
const env = z.object({
NODE_ENV: z.enum(["production", "development"]).default("production"),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().default("file:../../dev.db"),
SESSION_SECRET: z.string(),
});
export default env.parse(process.env);
+21
View File
@@ -0,0 +1,21 @@
import { z } from "zod";
export default {
users: {
registration: z.object({
name: z.string().min(1),
email: z.email().min(1),
password: z.string().min(8),
}),
},
posts: {
create: z.object({
title: z.string().min(1),
slug: z.string().min(1),
description: z.string().min(1),
content: z.string().min(1),
}),
delete: z.string(),
get: z.string().min(1),
},
};
+43
View File
@@ -0,0 +1,43 @@
{
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": {
// File Layout
"rootDir": "./src",
"outDir": "./dist",
// Environment Settings
// See also https://aka.ms/tsconfig/module
"module": "nodenext",
"target": "esnext",
// For nodejs:
"lib": ["esnext"],
"types": ["node"],
// and npm install -D @types/node
// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "blog",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "^11.9.0",
"onFail": "download"
}
},
"type": "module"
}
+6
View File
@@ -0,0 +1,6 @@
node_modules
# Keep environment variables out of version control
.env
/src/generated/prisma
dist/*
+36
View File
@@ -0,0 +1,36 @@
{
"name": "database",
"version": "1.0.0",
"description": "",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"db:deploy": "prisma migrate deploy",
"db:studio": "prisma studio",
"db:format": "prisma format",
"build": "tsc"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"devDependencies": {
"@types/node": "^26.4.1",
"tsx": "^4.23.13",
"typescript": "^7.0.2"
},
"dependencies": {
"@prisma/adapter-better-sqlite3": "^7.10.0",
"@prisma/client": "7.10.0",
"dotenv": "^17.4.2",
"prisma": "7.10.0"
}
}
@@ -0,0 +1,10 @@
-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"password" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
@@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "Post" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"content" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"userId" INTEGER,
CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
@@ -0,0 +1,25 @@
/*
Warnings:
- Added the required column `slug` to the `Post` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Post" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"content" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"userId" INTEGER,
CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_Post" ("content", "createdAt", "description", "id", "title", "updatedAt", "userId") SELECT "content", "createdAt", "description", "id", "title", "updatedAt", "userId" FROM "Post";
DROP TABLE "Post";
ALTER TABLE "new_Post" RENAME TO "Post";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"
+30
View File
@@ -0,0 +1,30 @@
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "sqlite"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
password String
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
description String
slug String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id])
userId Int?
}
+12
View File
@@ -0,0 +1,12 @@
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: env("DATABASE_URL"),
},
});
+21
View File
@@ -0,0 +1,21 @@
import "dotenv/config";
import { PrismaClient } from "./generated/prisma/client.js";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
const adapter = new PrismaBetterSqlite3({
url: process.env["DATABASE_URL"] as string,
});
const globalForPrisma = globalThis as typeof globalThis & {
prisma?: PrismaClient;
};
export const prisma: PrismaClient =
globalForPrisma.prisma ??
new PrismaClient({
adapter,
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
+2
View File
@@ -0,0 +1,2 @@
export { prisma } from "./client.js";
export * from "./generated/prisma/client.js";
+44
View File
@@ -0,0 +1,44 @@
{
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": {
// File Layout
"rootDir": "./src",
"outDir": "./dist",
// Environment Settings
// See also https://aka.ms/tsconfig/module
"module": "nodenext",
"target": "esnext",
// For nodejs:
"lib": ["esnext"],
"types": ["node"],
// and npm install -D @types/node
// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true
},
"include": ["src"]
}
+3033
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
packages:
- "apps/*"
- "packages/*"
allowBuilds:
"@prisma/engines": true
bcrypt: true
better-sqlite3: true
esbuild: false
prisma: true