mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-08-27 18:57:28 +00:00
Implement initial application structure and UI.
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/54918520-d42c-44cd-b4f4-9e5a42eb5be1.jpg
This commit is contained in:
+205
@@ -0,0 +1,205 @@
|
||||
import passport from "passport";
|
||||
import { Strategy as LocalStrategy } from "passport-local";
|
||||
import { Express, Request } from "express";
|
||||
import session from "express-session";
|
||||
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
|
||||
import { promisify } from "util";
|
||||
import { storage } from "./storage";
|
||||
import { User, InsertUser } from "@shared/schema";
|
||||
import { z } from "zod";
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface User extends User {}
|
||||
}
|
||||
}
|
||||
|
||||
const scryptAsync = promisify(scrypt);
|
||||
|
||||
async function hashPassword(password: string) {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const buf = (await scryptAsync(password, salt, 64)) as Buffer;
|
||||
return `${buf.toString("hex")}.${salt}`;
|
||||
}
|
||||
|
||||
async function comparePasswords(supplied: string, stored: string) {
|
||||
const [hashed, salt] = stored.split(".");
|
||||
const hashedBuf = Buffer.from(hashed, "hex");
|
||||
const suppliedBuf = (await scryptAsync(supplied, salt, 64)) as Buffer;
|
||||
return timingSafeEqual(hashedBuf, suppliedBuf);
|
||||
}
|
||||
|
||||
// API Token Authentication middleware
|
||||
export function authenticateApiToken(req: Request, res: any, next: any) {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
storage.getApiTokenByToken(token).then(apiToken => {
|
||||
if (apiToken && apiToken.isActive) {
|
||||
req.apiToken = apiToken;
|
||||
}
|
||||
next();
|
||||
}).catch(err => {
|
||||
console.error('API token authentication error:', err);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// Role-based authorization middleware for API and UI
|
||||
export function requireRole(roles: string[]) {
|
||||
return (req: Request, res: any, next: any) => {
|
||||
// Check if authenticated via session
|
||||
if (req.isAuthenticated() && req.user && roles.includes(req.user.role)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check if authenticated via API token
|
||||
if (req.apiToken) {
|
||||
const hasPermission = req.apiToken.permissions.some(p => roles.includes(p));
|
||||
if (hasPermission) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(403).json({ message: "Insufficient permissions" });
|
||||
};
|
||||
}
|
||||
|
||||
export function setupAuth(app: Express) {
|
||||
const sessionSettings: session.SessionOptions = {
|
||||
secret: process.env.SESSION_SECRET || 'dynamidns-secret',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: storage.sessionStore,
|
||||
cookie: {
|
||||
maxAge: 1000 * 60 * 60 * 24, // 1 day
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax'
|
||||
}
|
||||
};
|
||||
|
||||
app.set("trust proxy", 1);
|
||||
app.use(session(sessionSettings));
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
// Apply API token authentication middleware before routes
|
||||
app.use(authenticateApiToken);
|
||||
|
||||
passport.use(
|
||||
new LocalStrategy(async (username, password, done) => {
|
||||
try {
|
||||
// Check if username is email format
|
||||
const isEmail = username.includes('@');
|
||||
|
||||
let user;
|
||||
if (isEmail) {
|
||||
user = await storage.getUserByEmail(username);
|
||||
} else {
|
||||
user = await storage.getUserByUsername(username);
|
||||
}
|
||||
|
||||
if (!user || !(await comparePasswords(password, user.password))) {
|
||||
return done(null, false);
|
||||
} else {
|
||||
return done(null, user);
|
||||
}
|
||||
} catch (error) {
|
||||
return done(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
passport.serializeUser((user, done) => done(null, user.id));
|
||||
passport.deserializeUser(async (id: number, done) => {
|
||||
try {
|
||||
const user = await storage.getUser(id);
|
||||
done(null, user);
|
||||
} catch (error) {
|
||||
done(error);
|
||||
}
|
||||
});
|
||||
|
||||
// User registration validation schema
|
||||
const registerSchema = z.object({
|
||||
username: z.string().min(3).max(50),
|
||||
password: z.string().min(8).max(100),
|
||||
email: z.string().email(),
|
||||
fullName: z.string().optional(),
|
||||
organizationId: z.number().optional()
|
||||
});
|
||||
|
||||
app.post("/api/register", async (req, res, next) => {
|
||||
try {
|
||||
// Validate input
|
||||
const validatedData = registerSchema.parse(req.body);
|
||||
|
||||
// Check if user exists
|
||||
const existingUser = await storage.getUserByUsername(validatedData.username);
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ message: "Username already exists" });
|
||||
}
|
||||
|
||||
const existingEmail = await storage.getUserByEmail(validatedData.email);
|
||||
if (existingEmail) {
|
||||
return res.status(400).json({ message: "Email already exists" });
|
||||
}
|
||||
|
||||
// Create user
|
||||
const user = await storage.createUser({
|
||||
...validatedData,
|
||||
password: await hashPassword(validatedData.password),
|
||||
role: "user", // Default role
|
||||
});
|
||||
|
||||
// Remove password from response
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
|
||||
req.login(user, (err) => {
|
||||
if (err) return next(err);
|
||||
res.status(201).json(userWithoutPassword);
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/login", (req, res, next) => {
|
||||
passport.authenticate("local", (err, user, info) => {
|
||||
if (err) return next(err);
|
||||
if (!user) return res.status(401).json({ message: "Invalid credentials" });
|
||||
|
||||
req.login(user, (err) => {
|
||||
if (err) return next(err);
|
||||
|
||||
// Remove password from response
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
res.status(200).json(userWithoutPassword);
|
||||
});
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
app.post("/api/logout", (req, res, next) => {
|
||||
req.logout((err) => {
|
||||
if (err) return next(err);
|
||||
res.sendStatus(200);
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/user", (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.status(401).json({ message: "Not authenticated" });
|
||||
|
||||
// Remove password from response
|
||||
const { password, ...userWithoutPassword } = req.user;
|
||||
res.json(userWithoutPassword);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user