126 lines
3.1 KiB
TypeScript
126 lines
3.1 KiB
TypeScript
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 { RedisStore } from "connect-redis";
|
|
import { createClient, RedisClient } from "redis";
|
|
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";
|
|
|
|
// redis client for session mgmt
|
|
const redisClient = createClient({ url: env.REDIS_URL });
|
|
redisClient.connect().catch(console.error);
|
|
|
|
const app = express();
|
|
|
|
// TLS terminates at Traefik and the SSR frontend proxies onward over plain
|
|
// HTTP, so req.protocol is "http" here and the session cookie's `secure` flag
|
|
// would silently suppress Set-Cookie. Trusting X-Forwarded-Proto restores it.
|
|
// The only reachable client is that proxy, on an internal network.
|
|
app.set("trust proxy", true);
|
|
|
|
app.use(express.urlencoded({ extended: false }));
|
|
app.use(express.json());
|
|
|
|
// 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(
|
|
session({
|
|
store: new RedisStore({ client: redisClient, prefix: "sess:" }),
|
|
secret: env.SESSION_SECRET,
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
httpOnly: true,
|
|
sameSite: "lax",
|
|
secure: env.NODE_ENV === "production",
|
|
maxAge: 1000 * 60 * 60 * 24 * 7,
|
|
},
|
|
}),
|
|
);
|
|
app.use(passport.session());
|
|
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");
|
|
});
|