Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eb2cec1
feat: OAuth middleware with module loaders for identity/secrets/auth …
pyramation May 23, 2026
87b81ab
feat(oauth): integrate DB settings for state cookie and error redirect
theothersideofgod May 26, 2026
e1a1e75
fix(loaders): use config_secrets_org_module for encryptedSecrets loader
theothersideofgod May 26, 2026
ec0d307
feat(express-context): add sessionCredentialsSchemaName to userAuth l…
theothersideofgod May 26, 2026
4e5e67a
feat(express-context): add connectedAccounts loader
theothersideofgod May 26, 2026
bd90d93
feat(graphql-server): handle PgInterval in session cookie config
theothersideofgod May 26, 2026
1735802
feat(graphql-server): identity pre-check via connectedAccounts query
theothersideofgod May 26, 2026
f6ae4cd
fix(oauth): use JOIN to fetch client_secret instead of non-existent g…
theothersideofgod May 26, 2026
e7dc6d5
fix(express-context): use config_secrets_user_module for app_secrets …
theothersideofgod May 26, 2026
78a49e0
fix(express-context): use user_secrets instead of app_secrets
theothersideofgod May 29, 2026
414187b
feat(express-context): add identityProviderConfig loader
theothersideofgod May 29, 2026
7e14b77
refactor(oauth): use identityProviderConfig loader and remove duplicates
theothersideofgod May 29, 2026
5a371b2
fix: use platform_secrets instead of user_secrets for OAuth
theothersideofgods Jun 5, 2026
5ec998d
refactor: use loader function names for sign_in/sign_up_identity
theothersideofgods Jun 5, 2026
42574ec
fix(oauth): check identity existence before sign_in/sign_up
theothersideofgods Jun 5, 2026
16a396d
docs(oauth): add RLS bypass comment for generateCrossOriginToken
theothersideofgods Jun 8, 2026
2f02e3e
docs(oauth): add RLS bypass comment for connected_accounts identity c…
theothersideofgods Jun 8, 2026
c8d931c
refactor(env): use unified env config for oauth, captcha, upload
theothersideofgods Jun 8, 2026
c52a555
refactor(oauth): use QuoteUtils for SQL identifier quoting
theothersideofgods Jun 8, 2026
be2530c
feat(oauth): add emailVerified field to OAuthProfile
theothersideofgods Jun 8, 2026
b14274d
refactor(loaders): merge identityProviderConfig into identityProviders
theothersideofgods Jun 8, 2026
2fa0311
chore: regenerate pnpm-lock.yaml
theothersideofgods Jun 8, 2026
bf079c7
fix: remove identityProviderConfigLoader export and add @pgsql/quotes…
theothersideofgods Jun 8, 2026
5035a9c
fix: remove duplicate identityProviders destructuring
theothersideofgods Jun 8, 2026
fb3fe35
test: update env merge snapshots for oauth, captcha, upload fields
theothersideofgods Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions graphql/env/__tests__/__snapshots__/merge.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and
],
"roleName": "env_role",
},
"captcha": {},
"cdn": {
"awsAccessKey": "minioadmin",
"awsRegion": "us-east-1",
Expand Down Expand Up @@ -100,6 +101,7 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and
"useTx": false,
},
},
"oauth": {},
"pg": {
"database": "config-db",
"host": "override-host",
Expand All @@ -120,5 +122,6 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and
"port": 587,
"secure": false,
},
"upload": {},
}
`;
1 change: 1 addition & 0 deletions graphql/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@constructive-io/express-context": "workspace:^",
"@constructive-io/graphql-env": "workspace:^",
"@constructive-io/graphql-types": "workspace:^",
"@constructive-io/oauth": "workspace:^",
"@constructive-io/s3-utils": "workspace:^",
"@constructive-io/upload-names": "workspace:^",
"@constructive-io/url-domains": "workspace:^",
Expand Down
3 changes: 2 additions & 1 deletion graphql/server/src/middleware/captcha.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Logger } from '@pgpmjs/logger';
import { getEnvVars } from '@pgpmjs/env';
import type { NextFunction, Request, RequestHandler, Response } from 'express';
import './types'; // for Request type

Expand Down Expand Up @@ -94,7 +95,7 @@ export const createCaptchaMiddleware = (): RequestHandler => {
}

// Secret key must be set server-side (env var, not stored in DB for security)
const secretKey = process.env.RECAPTCHA_SECRET_KEY;
const secretKey = getEnvVars().captcha?.recaptchaSecretKey;
if (!secretKey) {
log.warn('[captcha] enable_captcha is true but RECAPTCHA_SECRET_KEY env var is not set; skipping verification');
return next();
Expand Down
27 changes: 22 additions & 5 deletions graphql/server/src/middleware/cookie.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import type { Request, Response } from 'express';
import type { AuthSettings } from '../types';
import type { AuthSettings, PgInterval } from '../types';

export const SESSION_COOKIE_NAME = 'constructive_session';
export const DEVICE_TOKEN_COOKIE_NAME = 'constructive_device_token';

const DEVICE_TOKEN_MAX_AGE = 90 * 24 * 60 * 60; // 90 days in seconds

export const parseIntervalToSeconds = (interval: string | PgInterval | null | undefined): number | null => {
if (!interval) return null;
if (typeof interval === 'string') {
const parsed = parseInt(interval, 10);
return isNaN(parsed) ? null : parsed;
}
let totalSeconds = 0;
if (interval.years) totalSeconds += interval.years * 365 * 24 * 60 * 60;
if (interval.months) totalSeconds += interval.months * 30 * 24 * 60 * 60;
if (interval.days) totalSeconds += interval.days * 24 * 60 * 60;
if (interval.hours) totalSeconds += interval.hours * 60 * 60;
if (interval.minutes) totalSeconds += interval.minutes * 60;
if (interval.seconds) totalSeconds += interval.seconds;
if (interval.milliseconds) totalSeconds += interval.milliseconds / 1000;
return totalSeconds > 0 ? totalSeconds : null;
};

export interface CookieConfig {
secure: boolean;
sameSite: 'strict' | 'lax' | 'none';
Expand All @@ -25,11 +42,11 @@ export const getSessionCookieConfig = (
const DEFAULT_MAX_AGE = 86400; // 24 hours
let maxAge = DEFAULT_MAX_AGE;
if (rememberMe && authSettings?.rememberMeDuration) {
const parsed = parseInt(authSettings.rememberMeDuration, 10);
if (!isNaN(parsed)) maxAge = parsed;
const parsed = parseIntervalToSeconds(authSettings.rememberMeDuration);
if (parsed !== null) maxAge = parsed;
} else if (authSettings?.cookieMaxAge) {
const parsed = parseInt(authSettings.cookieMaxAge, 10);
if (!isNaN(parsed)) maxAge = parsed;
const parsed = parseIntervalToSeconds(authSettings.cookieMaxAge);
if (parsed !== null) maxAge = parsed;
}

return {
Expand Down
Loading
Loading