init
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
*.db
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
|
||||
/src/generated/prisma
|
||||
dist/*
|
||||
@@ -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"
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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"),
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { prisma } from "./client.js";
|
||||
export * from "./generated/prisma/client.js";
|
||||
@@ -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"]
|
||||
}
|
||||
Generated
+3033
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
allowBuilds:
|
||||
"@prisma/engines": true
|
||||
bcrypt: true
|
||||
better-sqlite3: true
|
||||
esbuild: false
|
||||
prisma: true
|
||||
Reference in New Issue
Block a user