mirror of
https://github.com/fosrl/pangolin.git
synced 2025-05-12 21:30:35 +01:00
allow controlling cors from config and add cors middleware to traefik
This commit is contained in:
parent
7ff5376d13
commit
ab18e15a71
8 changed files with 98 additions and 43 deletions
|
@ -1,15 +1,15 @@
|
|||
app:
|
||||
dashboard_url: http://localhost
|
||||
dashboard_url: http://localhost:3002
|
||||
base_domain: localhost
|
||||
log_level: debug
|
||||
log_level: info
|
||||
save_logs: false
|
||||
|
||||
server:
|
||||
external_port: 3000
|
||||
internal_port: 3001
|
||||
next_port: 3002
|
||||
internal_hostname: localhost
|
||||
secure_cookies: false
|
||||
internal_hostname: pangolin
|
||||
secure_cookies: true
|
||||
session_cookie_name: p_session
|
||||
resource_session_cookie_name: p_resource_session
|
||||
resource_access_token_param: p_token
|
||||
|
@ -38,4 +38,6 @@ users:
|
|||
password: Password123!
|
||||
|
||||
flags:
|
||||
require_email_verification: false
|
||||
require_email_verification: true
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: true
|
||||
|
|
|
@ -4,6 +4,21 @@ http:
|
|||
redirectScheme:
|
||||
scheme: https
|
||||
permanent: true
|
||||
cors:
|
||||
headers:
|
||||
accessControlAllowMethods:
|
||||
- GET
|
||||
- PUT
|
||||
- POST
|
||||
- DELETE
|
||||
- PATCH
|
||||
accessControlAllowHeaders:
|
||||
- Content-Type
|
||||
- X-CSRF-Token
|
||||
accessControlAllowOriginList:
|
||||
- https://{{.DashboardDomain}}
|
||||
accessControlAllowCredentials: false
|
||||
|
||||
|
||||
routers:
|
||||
# HTTP to HTTPS redirect router
|
||||
|
@ -14,6 +29,7 @@ http:
|
|||
- web
|
||||
middlewares:
|
||||
- redirect-to-https
|
||||
- cors
|
||||
|
||||
# Next.js router (handles everything except API and WebSocket paths)
|
||||
next-router:
|
||||
|
@ -21,6 +37,8 @@ http:
|
|||
service: next-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- cors
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
|
@ -30,6 +48,8 @@ http:
|
|||
service: api-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- cors
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
|
@ -39,6 +59,8 @@ http:
|
|||
service: api-service
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- cors
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
|
|
|
@ -20,23 +20,30 @@ const externalPort = config.getRawConfig().server.external_port;
|
|||
export function createApiServer() {
|
||||
const apiServer = express();
|
||||
|
||||
// Middleware setup
|
||||
apiServer.set("trust proxy", 1);
|
||||
if (dev) {
|
||||
apiServer.use(
|
||||
cors({
|
||||
origin: `http://localhost:${config.getRawConfig().server.next_port}`,
|
||||
credentials: true
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const corsOptions = {
|
||||
origin: config.getRawConfig().app.dashboard_url,
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
|
||||
allowedHeaders: ["Content-Type", "X-CSRF-Token"]
|
||||
};
|
||||
|
||||
apiServer.use(cors(corsOptions));
|
||||
const corsConfig = config.getRawConfig().server.cors;
|
||||
|
||||
const options = {
|
||||
...(corsConfig?.origins
|
||||
? { origin: corsConfig.origins }
|
||||
: {
|
||||
origin: (origin: any, callback: any) => {
|
||||
callback(null, true);
|
||||
}
|
||||
}),
|
||||
...(corsConfig?.methods && { methods: corsConfig.methods }),
|
||||
...(corsConfig?.allowed_headers && {
|
||||
allowedHeaders: corsConfig.allowed_headers
|
||||
}),
|
||||
credentials: !(corsConfig?.credentials === false)
|
||||
};
|
||||
|
||||
logger.debug("Using CORS options", options);
|
||||
|
||||
apiServer.use(cors(options));
|
||||
|
||||
if (!dev) {
|
||||
apiServer.use(helmet());
|
||||
apiServer.use(csrfProtectionMiddleware);
|
||||
}
|
||||
|
@ -47,7 +54,8 @@ export function createApiServer() {
|
|||
if (!dev) {
|
||||
apiServer.use(
|
||||
rateLimitMiddleware({
|
||||
windowMin: config.getRawConfig().rate_limits.global.window_minutes,
|
||||
windowMin:
|
||||
config.getRawConfig().rate_limits.global.window_minutes,
|
||||
max: config.getRawConfig().rate_limits.global.max_requests,
|
||||
type: "IP_AND_PATH"
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
encodeBase32LowerCaseNoPadding,
|
||||
encodeHexLowerCase,
|
||||
encodeHexLowerCase
|
||||
} from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { Session, sessions, User, users } from "@server/db/schema";
|
||||
|
@ -9,8 +9,10 @@ import { eq } from "drizzle-orm";
|
|||
import config from "@server/lib/config";
|
||||
import type { RandomReader } from "@oslojs/crypto/random";
|
||||
import { generateRandomString } from "@oslojs/crypto/random";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name;
|
||||
export const SESSION_COOKIE_NAME =
|
||||
config.getRawConfig().server.session_cookie_name;
|
||||
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
||||
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
|
||||
export const COOKIE_DOMAIN = "." + config.getBaseDomain();
|
||||
|
@ -24,25 +26,25 @@ export function generateSessionToken(): string {
|
|||
|
||||
export async function createSession(
|
||||
token: string,
|
||||
userId: string,
|
||||
userId: string
|
||||
): Promise<Session> {
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token)),
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
const session: Session = {
|
||||
sessionId: sessionId,
|
||||
userId,
|
||||
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
||||
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime()
|
||||
};
|
||||
await db.insert(sessions).values(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function validateSessionToken(
|
||||
token: string,
|
||||
token: string
|
||||
): Promise<SessionValidationResult> {
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token)),
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
const result = await db
|
||||
.select({ user: users, session: sessions })
|
||||
|
@ -61,12 +63,12 @@ export async function validateSessionToken(
|
|||
}
|
||||
if (Date.now() >= session.expiresAt - SESSION_COOKIE_EXPIRES / 2) {
|
||||
session.expiresAt = new Date(
|
||||
Date.now() + SESSION_COOKIE_EXPIRES,
|
||||
Date.now() + SESSION_COOKIE_EXPIRES
|
||||
).getTime();
|
||||
await db
|
||||
.update(sessions)
|
||||
.set({
|
||||
expiresAt: session.expiresAt,
|
||||
expiresAt: session.expiresAt
|
||||
})
|
||||
.where(eq(sessions.sessionId, session.sessionId));
|
||||
}
|
||||
|
@ -81,26 +83,38 @@ export async function invalidateAllSessions(userId: string): Promise<void> {
|
|||
await db.delete(sessions).where(eq(sessions.userId, userId));
|
||||
}
|
||||
|
||||
export function serializeSessionCookie(token: string): string {
|
||||
if (SECURE_COOKIES) {
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
export function serializeSessionCookie(
|
||||
token: string,
|
||||
isSecure: boolean
|
||||
): string {
|
||||
if (isSecure) {
|
||||
logger.debug("Setting cookie for secure origin");
|
||||
if (SECURE_COOKIES) {
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
} else {
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
}
|
||||
} else {
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createBlankSessionTokenCookie(): string {
|
||||
if (SECURE_COOKIES) {
|
||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
export function createBlankSessionTokenCookie(isSecure: boolean): string {
|
||||
if (isSecure) {
|
||||
if (SECURE_COOKIES) {
|
||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
} else {
|
||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
}
|
||||
} else {
|
||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
|
||||
}
|
||||
}
|
||||
|
||||
const random: RandomReader = {
|
||||
read(bytes: Uint8Array): void {
|
||||
crypto.getRandomValues(bytes);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export function generateId(length: number): string {
|
||||
|
|
|
@ -38,7 +38,13 @@ const environmentSchema = z.object({
|
|||
secure_cookies: z.boolean(),
|
||||
session_cookie_name: z.string(),
|
||||
resource_session_cookie_name: z.string(),
|
||||
resource_access_token_param: z.string()
|
||||
resource_access_token_param: z.string(),
|
||||
cors: z.object({
|
||||
origins: z.array(z.string()).optional(),
|
||||
methods: z.array(z.string()).optional(),
|
||||
allowed_headers: z.array(z.string()).optional(),
|
||||
credentials: z.boolean().optional(),
|
||||
}).optional()
|
||||
}),
|
||||
traefik: z.object({
|
||||
http_entrypoint: z.string(),
|
||||
|
|
|
@ -120,7 +120,8 @@ export async function login(
|
|||
|
||||
const token = generateSessionToken();
|
||||
await createSession(token, existingUser.userId);
|
||||
const cookie = serializeSessionCookie(token);
|
||||
const isSecure = req.protocol === "https";
|
||||
const cookie = serializeSessionCookie(token, isSecure);
|
||||
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
|
||||
|
|
|
@ -27,7 +27,8 @@ export async function logout(
|
|||
|
||||
try {
|
||||
await invalidateSession(sessionId);
|
||||
res.setHeader("Set-Cookie", createBlankSessionTokenCookie());
|
||||
const isSecure = req.protocol === "https";
|
||||
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
|
||||
|
||||
return response<null>(res, {
|
||||
data: null,
|
||||
|
|
|
@ -158,7 +158,8 @@ export async function signup(
|
|||
|
||||
const token = generateSessionToken();
|
||||
await createSession(token, userId);
|
||||
const cookie = serializeSessionCookie(token);
|
||||
const isSecure = req.protocol === "https";
|
||||
const cookie = serializeSessionCookie(token, isSecure);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
|
||||
if (config.getRawConfig().flags?.require_email_verification) {
|
||||
|
|
Loading…
Reference in a new issue