videofolxtv/server/replitAuth.ts
sebastjanartic c71720454f Add admin dashboard and Replit authentication integration
Integrates Replit authentication using OpenID Connect, adds an admin dashboard route with video management and thumbnail upload capabilities, and updates dependencies.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 890577b1-c154-40a4-a177-a0c6d55320c3
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/890577b1-c154-40a4-a177-a0c6d55320c3/1jMBtLj
2025-09-02 12:01:00 +00:00

172 lines
4.7 KiB
TypeScript

import * as client from "openid-client";
import { Strategy, type VerifyFunction } from "openid-client/passport";
import passport from "passport";
import session from "express-session";
import type { Express, RequestHandler } from "express";
import memoize from "memoizee";
import connectPg from "connect-pg-simple";
import { storage } from "./storage";
if (!process.env.REPLIT_DOMAINS) {
throw new Error("Environment variable REPLIT_DOMAINS not provided");
}
const getOidcConfig = memoize(
async () => {
return await client.discovery(
new URL(process.env.ISSUER_URL ?? "https://replit.com/oidc"),
process.env.REPL_ID!
);
},
{ maxAge: 3600 * 1000 }
);
export function getSession() {
const sessionTtl = 7 * 24 * 60 * 60 * 1000; // 1 week
const pgStore = connectPg(session);
const sessionStore = new pgStore({
conString: process.env.DATABASE_URL,
createTableIfMissing: false,
ttl: sessionTtl,
tableName: "sessions",
});
return session({
secret: process.env.SESSION_SECRET!,
store: sessionStore,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
maxAge: sessionTtl,
},
});
}
function updateUserSession(
user: any,
tokens: client.TokenEndpointResponse & client.TokenEndpointResponseHelpers
) {
user.claims = tokens.claims();
user.access_token = tokens.access_token;
user.refresh_token = tokens.refresh_token;
user.expires_at = user.claims?.exp;
}
async function upsertUser(claims: any) {
await storage.upsertUser({
id: claims["sub"],
email: claims["email"],
firstName: claims["first_name"],
lastName: claims["last_name"],
profileImageUrl: claims["profile_image_url"],
});
}
export async function setupAuth(app: Express) {
app.set("trust proxy", 1);
app.use(getSession());
app.use(passport.initialize());
app.use(passport.session());
const config = await getOidcConfig();
const verify: VerifyFunction = async (
tokens: client.TokenEndpointResponse & client.TokenEndpointResponseHelpers,
verified: passport.AuthenticateCallback
) => {
const user = {};
updateUserSession(user, tokens);
await upsertUser(tokens.claims());
verified(null, user);
};
for (const domain of process.env.REPLIT_DOMAINS!.split(",")) {
const strategy = new Strategy(
{
name: `replitauth:${domain}`,
config,
scope: "openid email profile offline_access",
callbackURL: `https://${domain}/api/callback`,
},
verify,
);
passport.use(strategy);
}
passport.serializeUser((user: Express.User, cb) => cb(null, user));
passport.deserializeUser((user: Express.User, cb) => cb(null, user));
app.get("/api/login", (req, res, next) => {
passport.authenticate(`replitauth:${req.hostname}`, {
prompt: "login consent",
scope: ["openid", "email", "profile", "offline_access"],
})(req, res, next);
});
app.get("/api/callback", (req, res, next) => {
passport.authenticate(`replitauth:${req.hostname}`, {
successReturnToOrRedirect: "/admin",
failureRedirect: "/api/login",
})(req, res, next);
});
app.get("/api/logout", (req, res) => {
req.logout(() => {
res.redirect(
client.buildEndSessionUrl(config, {
client_id: process.env.REPL_ID!,
post_logout_redirect_uri: `${req.protocol}://${req.hostname}`,
}).href
);
});
});
}
export const isAuthenticated: RequestHandler = async (req, res, next) => {
const user = req.user as any;
if (!req.isAuthenticated() || !user.expires_at) {
return res.status(401).json({ message: "Unauthorized" });
}
const now = Math.floor(Date.now() / 1000);
if (now <= user.expires_at) {
return next();
}
const refreshToken = user.refresh_token;
if (!refreshToken) {
res.status(401).json({ message: "Unauthorized" });
return;
}
try {
const config = await getOidcConfig();
const tokenResponse = await client.refreshTokenGrant(config, refreshToken);
updateUserSession(user, tokenResponse);
return next();
} catch (error) {
res.status(401).json({ message: "Unauthorized" });
return;
}
};
export const isAdmin: RequestHandler = async (req, res, next) => {
const user = req.user as any;
if (!req.isAuthenticated() || !user.claims?.sub) {
return res.status(401).json({ message: "Unauthorized" });
}
try {
const dbUser = await storage.getUser(user.claims.sub);
if (!dbUser || !dbUser.isAdmin) {
return res.status(403).json({ message: "Admin access required" });
}
next();
} catch (error) {
console.error("Error checking admin status:", error);
res.status(500).json({ message: "Internal server error" });
}
};