init
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.env
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { User as PrismaUser } from "database";
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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),
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user