Files
demat-facturation/server/auth.ts

167 lines
4.1 KiB
TypeScript

import bcrypt from "bcrypt";
import { ConfidentialClientApplication } from "@azure/msal-node";
import { getUserByEmail } from "./db";
import jwt from "jsonwebtoken";
const SALT_ROUNDS = 10;
// ============= LOCAL AUTHENTICATION =============
/**
* Hash a password using bcrypt
*/
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
/**
* Verify a password against a hash
*/
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
/**
* Authenticate a user with email and password (local auth)
* Returns user and JWT token if successful, null otherwise
*/
export async function loginLocal(email: string, password: string) {
const user = await getUserByEmail(email);
if (!user) {
return null;
}
// Check if user is active
if (user.isActive === 0) {
throw new Error("Account is inactive");
}
// Check if user has a password (local auth)
if (!user.passwordHash) {
throw new Error("This account does not support local authentication");
}
// Verify password
const isValid = await verifyPassword(password, user.passwordHash);
if (!isValid) {
return null;
}
// Generate JWT token
const token = generateToken(user);
return { user, token };
}
// ============= JWT TOKEN GENERATION =============
/**
* Generate a JWT token for a user
*/
export function generateToken(user: { id: number; email: string; role: string }): string {
const secret = process.env.JWT_SECRET || "default-secret-change-in-production";
return jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role,
},
secret,
{ expiresIn: "7d" }
);
}
/**
* Verify and decode a JWT token
*/
export function verifyToken(token: string): { userId: number; email: string; role: string } | null {
try {
const secret = process.env.JWT_SECRET || "default-secret-change-in-production";
const decoded = jwt.verify(token, secret) as { userId: number; email: string; role: string };
return decoded;
} catch (error) {
return null;
}
}
// ============= AZURE AD AUTHENTICATION =============
let msalClient: ConfidentialClientApplication | null = null;
/**
* Check if Azure AD is configured
*/
export function isAzureAdConfigured(): boolean {
return !!(
process.env.AZURE_AD_TENANT_ID &&
process.env.AZURE_AD_CLIENT_ID &&
process.env.AZURE_AD_CLIENT_SECRET
);
}
/**
* Get MSAL client instance (lazy initialization)
*/
function getMsalClient(): ConfidentialClientApplication {
if (!isAzureAdConfigured()) {
throw new Error("Azure AD is not configured");
}
if (!msalClient) {
msalClient = new ConfidentialClientApplication({
auth: {
clientId: process.env.AZURE_AD_CLIENT_ID!,
authority: `https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}`,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET!,
},
});
}
return msalClient;
}
/**
* Get Azure AD authorization URL for user login
*/
export async function getAzureAuthUrl(): Promise<string> {
const client = getMsalClient();
const redirectUri = process.env.AZURE_AD_REDIRECT_URI || "http://localhost:3000/api/auth/azure/callback";
const authCodeUrlParameters = {
scopes: ["user.read"],
redirectUri,
};
return client.getAuthCodeUrl(authCodeUrlParameters);
}
/**
* Handle Azure AD callback and exchange code for tokens
*/
export async function handleAzureCallback(code: string) {
const client = getMsalClient();
const redirectUri = process.env.AZURE_AD_REDIRECT_URI || "http://localhost:3000/api/auth/azure/callback";
const tokenRequest = {
code,
scopes: ["user.read"],
redirectUri,
};
const response = await client.acquireTokenByCode(tokenRequest);
if (!response || !response.account) {
throw new Error("Failed to acquire token from Azure AD");
}
return {
azureAdId: response.account.homeAccountId,
email: response.account.username,
name: response.account.name || response.account.username,
};
}