Enrollment + almost all auth
This commit is contained in:
14
lib/appUrl.ts
Normal file
14
lib/appUrl.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export function getBaseUrl(req?: Request) {
|
||||
const envUrl = process.env.APP_BASE_URL || process.env.NEXT_PUBLIC_APP_URL;
|
||||
if (envUrl) return String(envUrl).replace(/\/+$/, "");
|
||||
if (!req) return "http://localhost:3000";
|
||||
|
||||
const forwardedProto = req.headers.get("x-forwarded-proto");
|
||||
const proto = forwardedProto ? forwardedProto.split(",")[0].trim() : new URL(req.url).protocol.replace(":", "");
|
||||
const host =
|
||||
req.headers.get("x-forwarded-host") ||
|
||||
req.headers.get("host") ||
|
||||
new URL(req.url).host;
|
||||
|
||||
return `${proto}://${host}`;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ const COOKIE_NAME = "mis_session";
|
||||
export async function requireSession() {
|
||||
const jar = await cookies();
|
||||
const sessionId = jar.get(COOKIE_NAME)?.value;
|
||||
if (!sessionId) throw new Error("UNAUTHORIZED");
|
||||
if (!sessionId) return null;
|
||||
|
||||
const session = await prisma.session.findFirst({
|
||||
where: {
|
||||
@@ -14,9 +14,21 @@ export async function requireSession() {
|
||||
revokedAt: null,
|
||||
expiresAt: { gt: new Date() },
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: { isActive: true, emailVerifiedAt: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!session) throw new Error("UNAUTHORIZED");
|
||||
if (!session) return null;
|
||||
|
||||
if (!session.user?.isActive || !session.user?.emailVerifiedAt) {
|
||||
await prisma.session
|
||||
.update({ where: { id: session.id }, data: { revokedAt: new Date() } })
|
||||
.catch(() => {});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Optional: update lastSeenAt (useful later)
|
||||
await prisma.session
|
||||
|
||||
20
lib/auth/sessionCookie.ts
Normal file
20
lib/auth/sessionCookie.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const COOKIE_NAME = "mis_session";
|
||||
export const SESSION_DAYS = 7;
|
||||
|
||||
export function isSecureRequest(req: Request) {
|
||||
const forwardedProto = req.headers.get("x-forwarded-proto");
|
||||
if (forwardedProto) {
|
||||
return forwardedProto.split(",")[0].trim() === "https";
|
||||
}
|
||||
return new URL(req.url).protocol === "https:";
|
||||
}
|
||||
|
||||
export function buildSessionCookieOptions(req: Request) {
|
||||
return {
|
||||
httpOnly: true,
|
||||
sameSite: "lax" as const,
|
||||
secure: isSecureRequest(req),
|
||||
path: "/",
|
||||
maxAge: SESSION_DAYS * 24 * 60 * 60,
|
||||
};
|
||||
}
|
||||
86
lib/email.ts
Normal file
86
lib/email.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
type EmailPayload = {
|
||||
to: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
html: string;
|
||||
};
|
||||
|
||||
let cachedTransport: nodemailer.Transporter | null = null;
|
||||
|
||||
function getTransporter() {
|
||||
if (cachedTransport) return cachedTransport;
|
||||
|
||||
const host = process.env.SMTP_HOST;
|
||||
const port = Number(process.env.SMTP_PORT || 465);
|
||||
const user = process.env.SMTP_USER;
|
||||
const pass = process.env.SMTP_PASS;
|
||||
const secure =
|
||||
process.env.SMTP_SECURE !== undefined
|
||||
? process.env.SMTP_SECURE === "true"
|
||||
: port === 465;
|
||||
|
||||
if (!host || !user || !pass) {
|
||||
throw new Error("SMTP not configured");
|
||||
}
|
||||
|
||||
cachedTransport = nodemailer.createTransport({
|
||||
host,
|
||||
port,
|
||||
secure,
|
||||
auth: { user, pass },
|
||||
});
|
||||
|
||||
return cachedTransport;
|
||||
}
|
||||
|
||||
export async function sendEmail(payload: EmailPayload) {
|
||||
const from = process.env.SMTP_FROM || process.env.SMTP_USER;
|
||||
if (!from) {
|
||||
throw new Error("SMTP_FROM not configured");
|
||||
}
|
||||
|
||||
const transporter = getTransporter();
|
||||
return transporter.sendMail({
|
||||
from,
|
||||
to: payload.to,
|
||||
subject: payload.subject,
|
||||
text: payload.text,
|
||||
html: payload.html,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildVerifyEmail(params: { appName: string; verifyUrl: string }) {
|
||||
const subject = `Verify your ${params.appName} account`;
|
||||
const text =
|
||||
`Welcome to ${params.appName}.\n\n` +
|
||||
`Verify your email to activate your account:\n${params.verifyUrl}\n\n` +
|
||||
`If you did not request this, ignore this email.`;
|
||||
const html =
|
||||
`<p>Welcome to ${params.appName}.</p>` +
|
||||
`<p>Verify your email to activate your account:</p>` +
|
||||
`<p><a href="${params.verifyUrl}">${params.verifyUrl}</a></p>` +
|
||||
`<p>If you did not request this, ignore this email.</p>`;
|
||||
|
||||
return { subject, text, html };
|
||||
}
|
||||
|
||||
export function buildInviteEmail(params: {
|
||||
appName: string;
|
||||
orgName: string;
|
||||
inviteUrl: string;
|
||||
}) {
|
||||
const subject = `You're invited to ${params.orgName} on ${params.appName}`;
|
||||
const text =
|
||||
`You have been invited to join ${params.orgName} on ${params.appName}.\n\n` +
|
||||
`Accept the invite here:\n${params.inviteUrl}\n\n` +
|
||||
`If you did not expect this invite, you can ignore this email.`;
|
||||
const html =
|
||||
`<p>You have been invited to join ${params.orgName} on ${params.appName}.</p>` +
|
||||
`<p>Accept the invite here:</p>` +
|
||||
`<p><a href="${params.inviteUrl}">${params.inviteUrl}</a></p>` +
|
||||
`<p>If you did not expect this invite, you can ignore this email.</p>`;
|
||||
|
||||
return { subject, text, html };
|
||||
}
|
||||
16
lib/pairingCode.ts
Normal file
16
lib/pairingCode.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
const PAIRING_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
|
||||
export function generatePairingCode(length = 5) {
|
||||
const bytes = randomBytes(length);
|
||||
let code = "";
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
code += PAIRING_ALPHABET[bytes[i] % PAIRING_ALPHABET.length];
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
export function normalizePairingCode(input: string) {
|
||||
return input.trim().toUpperCase().replace(/[^A-Z0-9]/g, "");
|
||||
}
|
||||
Reference in New Issue
Block a user