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
+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
}
}