Compare commits
8 Commits
fix/wordpr
...
fix/auth-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fa9ea496c | ||
|
|
31756a2233 | ||
|
|
166583621b | ||
|
|
ca952c4674 | ||
|
|
4054778b6c | ||
|
|
56a5f00015 | ||
|
|
f25bebf6ee | ||
|
|
22dad6d0fc |
@@ -90,10 +90,10 @@ steps:
|
||||
platforms: linux/amd64
|
||||
provenance: false
|
||||
build_args:
|
||||
- APP_BUILD_VERSION=${CI_COMMIT_SHA:0:8}
|
||||
- APP_GIT_SHA=${CI_COMMIT_SHA}
|
||||
- APP_BUILD_TIME=${CI_PIPELINE_CREATED}
|
||||
- CONTAINER_IMAGE_TAG=${CI_COMMIT_SHA:0:8}
|
||||
APP_BUILD_VERSION: ${CI_COMMIT_SHA:0:8}
|
||||
APP_GIT_SHA: ${CI_COMMIT_SHA}
|
||||
APP_BUILD_TIME: ${CI_PIPELINE_CREATED}
|
||||
CONTAINER_IMAGE_TAG: ${CI_COMMIT_SHA:0:8}
|
||||
depends_on: []
|
||||
when:
|
||||
branch: master
|
||||
|
||||
BIN
backend/public/downloads/cannaiq-menus-1.6.0.zip
Normal file
BIN
backend/public/downloads/cannaiq-menus-1.6.0.zip
Normal file
Binary file not shown.
1
backend/public/downloads/cannaiq-menus-latest.zip
Symbolic link
1
backend/public/downloads/cannaiq-menus-latest.zip
Symbolic link
@@ -0,0 +1 @@
|
||||
cannaiq-menus-1.6.0.zip
|
||||
@@ -32,6 +32,7 @@ const TRUSTED_ORIGINS = [
|
||||
// Pattern-based trusted origins (wildcards)
|
||||
const TRUSTED_ORIGIN_PATTERNS = [
|
||||
/^https:\/\/.*\.cannabrands\.app$/, // *.cannabrands.app
|
||||
/^https:\/\/.*\.cannaiq\.co$/, // *.cannaiq.co
|
||||
];
|
||||
|
||||
// Trusted IPs for internal pod-to-pod communication
|
||||
@@ -152,7 +153,53 @@ export async function authenticateUser(email: string, password: string): Promise
|
||||
}
|
||||
|
||||
export async function authMiddleware(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
// Allow trusted origins/IPs to bypass auth (internal services, same-origin)
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
// If a Bearer token is provided, always try to use it first (logged-in user)
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
// Try JWT first
|
||||
const jwtUser = verifyToken(token);
|
||||
|
||||
if (jwtUser) {
|
||||
req.user = jwtUser;
|
||||
return next();
|
||||
}
|
||||
|
||||
// If JWT fails, try API token
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT id, name, rate_limit, active, expires_at, allowed_endpoints
|
||||
FROM api_tokens
|
||||
WHERE token = $1
|
||||
`, [token]);
|
||||
|
||||
if (result.rows.length > 0) {
|
||||
const apiToken = result.rows[0];
|
||||
if (!apiToken.active) {
|
||||
return res.status(401).json({ error: 'API token is inactive' });
|
||||
}
|
||||
if (apiToken.expires_at && new Date(apiToken.expires_at) < new Date()) {
|
||||
return res.status(401).json({ error: 'API token has expired' });
|
||||
}
|
||||
req.user = {
|
||||
id: 0,
|
||||
email: `api:${apiToken.name}`,
|
||||
role: 'api_token'
|
||||
};
|
||||
req.apiToken = apiToken;
|
||||
return next();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('API token lookup error:', err);
|
||||
}
|
||||
|
||||
// Token provided but invalid
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
|
||||
// No token provided - check trusted origins for API access (WordPress, etc.)
|
||||
if (isTrustedRequest(req)) {
|
||||
req.user = {
|
||||
id: 0,
|
||||
@@ -162,80 +209,10 @@ export async function authMiddleware(req: AuthRequest, res: Response, next: Next
|
||||
return next();
|
||||
}
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'No token provided' });
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
// Try JWT first
|
||||
const jwtUser = verifyToken(token);
|
||||
|
||||
if (jwtUser) {
|
||||
req.user = jwtUser;
|
||||
return next();
|
||||
}
|
||||
|
||||
// If JWT fails, try API token
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT id, name, rate_limit, active, expires_at, allowed_endpoints
|
||||
FROM api_tokens
|
||||
WHERE token = $1
|
||||
`, [token]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
|
||||
const apiToken = result.rows[0];
|
||||
|
||||
// Check if token is active
|
||||
if (!apiToken.active) {
|
||||
return res.status(401).json({ error: 'Token is disabled' });
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
if (apiToken.expires_at && new Date(apiToken.expires_at) < new Date()) {
|
||||
return res.status(401).json({ error: 'Token has expired' });
|
||||
}
|
||||
|
||||
// Check allowed endpoints
|
||||
if (apiToken.allowed_endpoints && apiToken.allowed_endpoints.length > 0) {
|
||||
const isAllowed = apiToken.allowed_endpoints.some((pattern: string) => {
|
||||
// Simple wildcard matching
|
||||
const regex = new RegExp('^' + pattern.replace('*', '.*') + '$');
|
||||
return regex.test(req.path);
|
||||
});
|
||||
|
||||
if (!isAllowed) {
|
||||
return res.status(403).json({ error: 'Endpoint not allowed for this token' });
|
||||
}
|
||||
}
|
||||
|
||||
// Set API token on request for tracking
|
||||
req.apiToken = {
|
||||
id: apiToken.id,
|
||||
name: apiToken.name,
|
||||
rate_limit: apiToken.rate_limit
|
||||
};
|
||||
|
||||
// Set a generic user for compatibility with existing code
|
||||
req.user = {
|
||||
id: apiToken.id,
|
||||
email: `api-token-${apiToken.id}@system`,
|
||||
role: 'api'
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Error verifying API token:', error);
|
||||
return res.status(500).json({ error: 'Authentication failed' });
|
||||
}
|
||||
return res.status(401).json({ error: 'No token provided' });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Require specific role(s) to access endpoint.
|
||||
*
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Request, Response, NextFunction } from 'express';
|
||||
* These are our own frontends that should have unrestricted access.
|
||||
*/
|
||||
const TRUSTED_DOMAINS = [
|
||||
'cannaiq.co',
|
||||
'www.cannaiq.co',
|
||||
'*.cannaiq.co',
|
||||
'*.cannabrands.app',
|
||||
'findagram.co',
|
||||
'www.findagram.co',
|
||||
'findadispo.com',
|
||||
@@ -32,6 +32,24 @@ function extractDomain(header: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a domain matches any trusted domain (supports *.domain.com wildcards)
|
||||
*/
|
||||
function isTrustedDomain(domain: string): boolean {
|
||||
for (const trusted of TRUSTED_DOMAINS) {
|
||||
if (trusted.startsWith('*.')) {
|
||||
// Wildcard: *.example.com matches example.com and any subdomain
|
||||
const baseDomain = trusted.slice(2);
|
||||
if (domain === baseDomain || domain.endsWith('.' + baseDomain)) {
|
||||
return true;
|
||||
}
|
||||
} else if (domain === trusted) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the request comes from a trusted domain
|
||||
*/
|
||||
@@ -42,7 +60,7 @@ function isRequestFromTrustedDomain(req: Request): boolean {
|
||||
// Check Origin header first (preferred for CORS requests)
|
||||
if (origin) {
|
||||
const domain = extractDomain(origin);
|
||||
if (domain && TRUSTED_DOMAINS.includes(domain)) {
|
||||
if (domain && isTrustedDomain(domain)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -50,7 +68,7 @@ function isRequestFromTrustedDomain(req: Request): boolean {
|
||||
// Fallback to Referer header
|
||||
if (referer) {
|
||||
const domain = extractDomain(referer);
|
||||
if (domain && TRUSTED_DOMAINS.includes(domain)) {
|
||||
if (domain && isTrustedDomain(domain)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,12 @@ const CONSUMER_TRUSTED_ORIGINS = [
|
||||
'http://localhost:3002',
|
||||
];
|
||||
|
||||
// Wildcard trusted origin patterns (*.domain.com)
|
||||
const CONSUMER_TRUSTED_PATTERNS = [
|
||||
/^https:\/\/([a-z0-9-]+\.)?cannaiq\.co$/,
|
||||
/^https:\/\/([a-z0-9-]+\.)?cannabrands\.app$/,
|
||||
];
|
||||
|
||||
// Trusted IPs for local development (bypass API key auth)
|
||||
const TRUSTED_IPS = ['127.0.0.1', '::1', '::ffff:127.0.0.1'];
|
||||
|
||||
@@ -150,8 +156,17 @@ function isConsumerTrustedRequest(req: Request): boolean {
|
||||
return true;
|
||||
}
|
||||
const origin = req.headers.origin;
|
||||
if (origin && CONSUMER_TRUSTED_ORIGINS.includes(origin)) {
|
||||
return true;
|
||||
if (origin) {
|
||||
// Check exact matches
|
||||
if (CONSUMER_TRUSTED_ORIGINS.includes(origin)) {
|
||||
return true;
|
||||
}
|
||||
// Check wildcard patterns
|
||||
for (const pattern of CONSUMER_TRUSTED_PATTERNS) {
|
||||
if (pattern.test(origin)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const referer = req.headers.referer;
|
||||
if (referer) {
|
||||
@@ -160,6 +175,18 @@ function isConsumerTrustedRequest(req: Request): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Check wildcard patterns against referer origin
|
||||
try {
|
||||
const refererUrl = new URL(referer);
|
||||
const refererOrigin = refererUrl.origin;
|
||||
for (const pattern of CONSUMER_TRUSTED_PATTERNS) {
|
||||
if (pattern.test(refererOrigin)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Invalid referer URL, ignore
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user