Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
73 changes: 25 additions & 48 deletions .github/workflows/preview-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,36 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile

- name: Create Convex Preview
id: convex-create
- name: Deploy Convex Preview
id: convex-deploy
working-directory: apps/server
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_PREVIEW_DEPLOY_KEY }}
run: |
PR_NUM=${{ github.event.pull_request.number }}
echo "🚀 Creating Convex preview for PR #${PR_NUM}..."
echo "🚀 Deploying Convex preview for PR #${PR_NUM}..."

# First deploy creates the preview (social providers are optional,
# so this succeeds even without env vars set yet)
OUTPUT_FILE=$(mktemp)
set +e
bunx convex deploy --preview-create "pr-${PR_NUM}" 2>&1 | tee "$OUTPUT_FILE"
set -e

CONVEX_URL=$(grep -o 'https://[a-z0-9-]*\.convex\.cloud' "$OUTPUT_FILE" | head -1)
if [ -n "$CONVEX_URL" ]; then
echo "convex_url=$CONVEX_URL" >> $GITHUB_OUTPUT
CONVEX_SITE_URL=$(echo "$CONVEX_URL" | sed 's/\.convex\.cloud/.convex.site/')
echo "convex_site_url=$CONVEX_SITE_URL" >> $GITHUB_OUTPUT
echo " Cloud URL: $CONVEX_URL"
else
echo "⚠️ Could not extract URL, will retry after setting env vars"

if [ -z "$CONVEX_URL" ]; then
echo "❌ Failed to extract Convex URL"
exit 1
fi

CONVEX_SITE_URL=$(echo "$CONVEX_URL" | sed 's/\.convex\.cloud/.convex.site/')

echo "✅ Convex Preview deployed!"
echo " Cloud URL: $CONVEX_URL"
echo " Site URL: $CONVEX_SITE_URL"

echo "convex_url=$CONVEX_URL" >> $GITHUB_OUTPUT
echo "convex_site_url=$CONVEX_SITE_URL" >> $GITHUB_OUTPUT

- name: Set Convex Environment Variables
working-directory: apps/server
env:
Expand All @@ -63,45 +69,16 @@ jobs:
PR_NUM=${{ github.event.pull_request.number }}
RAILWAY_URL="https://web-openchat-pr-${PR_NUM}.up.railway.app"

bunx convex env set SITE_URL "$RAILWAY_URL" --preview-name pr-${PR_NUM}
bunx convex env set NEXT_PUBLIC_DEPLOYMENT "preview" --preview-name pr-${PR_NUM}
bunx convex env set BETTER_AUTH_SECRET "$BETTER_AUTH_SECRET" --preview-name pr-${PR_NUM}
bunx convex env set GITHUB_CLIENT_ID "$GITHUB_CLIENT_ID" --preview-name pr-${PR_NUM}
bunx convex env set GITHUB_CLIENT_SECRET "$GITHUB_CLIENT_SECRET" --preview-name pr-${PR_NUM}
bunx convex env set VERCEL_CLIENT_ID "$VERCEL_CLIENT_ID" --preview-name pr-${PR_NUM}
bunx convex env set VERCEL_CLIENT_SECRET "$VERCEL_CLIENT_SECRET" --preview-name pr-${PR_NUM}
bunx convex env set PRODUCTION_CONVEX_SITE_URL "https://outgoing-setter-201.convex.site" --preview-name pr-${PR_NUM}
bunx convex env set SITE_URL "$RAILWAY_URL" --preview-name "pr-${PR_NUM}"
bunx convex env set BETTER_AUTH_SECRET "$BETTER_AUTH_SECRET" --preview-name "pr-${PR_NUM}"
bunx convex env set GITHUB_CLIENT_ID "$GITHUB_CLIENT_ID" --preview-name "pr-${PR_NUM}"
bunx convex env set GITHUB_CLIENT_SECRET "$GITHUB_CLIENT_SECRET" --preview-name "pr-${PR_NUM}"
bunx convex env set VERCEL_CLIENT_ID "$VERCEL_CLIENT_ID" --preview-name "pr-${PR_NUM}"
bunx convex env set VERCEL_CLIENT_SECRET "$VERCEL_CLIENT_SECRET" --preview-name "pr-${PR_NUM}"
bunx convex env set PRODUCTION_CONVEX_SITE_URL "https://outgoing-setter-201.convex.site" --preview-name "pr-${PR_NUM}"

echo "✅ Convex env vars set for pr-${PR_NUM}"

- name: Deploy Convex Preview
id: convex-deploy
working-directory: apps/server
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_PREVIEW_DEPLOY_KEY }}
run: |
PR_NUM=${{ github.event.pull_request.number }}
echo "🚀 Deploying code to Convex preview pr-${PR_NUM}..."

OUTPUT_FILE=$(mktemp)
bunx convex deploy --preview-create "pr-${PR_NUM}" 2>&1 | tee "$OUTPUT_FILE"

CONVEX_URL=$(grep -o 'https://[a-z0-9-]*\.convex\.cloud' "$OUTPUT_FILE" | head -1)

if [ -z "$CONVEX_URL" ]; then
echo "❌ Failed to extract Convex URL"
exit 1
fi

CONVEX_SITE_URL=$(echo "$CONVEX_URL" | sed 's/\.convex\.cloud/.convex.site/')

echo "✅ Convex Preview deployed!"
echo " Cloud URL: $CONVEX_URL"
echo " Site URL: $CONVEX_SITE_URL"

echo "convex_url=$CONVEX_URL" >> $GITHUB_OUTPUT
echo "convex_site_url=$CONVEX_SITE_URL" >> $GITHUB_OUTPUT

- name: Configure Railway Environment
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
Expand Down
26 changes: 17 additions & 9 deletions apps/server/convex/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { oAuthProxy } from "better-auth/plugins";
import { components } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";
import { query } from "./_generated/server";
import { requireEnv } from "./env";

import { getAllowedOrigins } from "./lib/origins";

/**
Expand Down Expand Up @@ -85,17 +85,25 @@ export const createAuth = (
// Use Convex site URL as baseURL so OAuth callbacks work correctly
baseURL: convexSiteUrl,
database: authComponent.adapter(ctx),
// GitHub OAuth only - no email/password
// TODO: add email verification (requireEmailVerification + sendVerificationEmail)
emailAndPassword: {
enabled: false,
enabled: true,
minPasswordLength: 8,
maxPasswordLength: 128,
},
socialProviders: {
github: {
clientId: requireEnv("GITHUB_CLIENT_ID"),
clientSecret: requireEnv("GITHUB_CLIENT_SECRET"),
// Use current environment's URL for OAuth callbacks
redirectURI: `${convexSiteUrl}/api/auth/callback/github`,
},
// Only include GitHub OAuth if credentials are configured
// (avoids throwing during Convex module analysis when env vars aren't set yet)
...(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET
? {
github: {
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
// Use current environment's URL for OAuth callbacks
redirectURI: `${convexSiteUrl}/api/auth/callback/github`,
},
}
: {}),
// Only include Vercel OAuth if credentials are configured
...(process.env.VERCEL_CLIENT_ID && process.env.VERCEL_CLIENT_SECRET
? {
Expand Down
38 changes: 27 additions & 11 deletions apps/server/convex/lib/origins.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
const BASE_ALLOWED_ORIGINS = [
const ALLOWED_ORIGINS = [
"http://localhost:3000",
"https://osschat.dev",
"*.osschat.dev",
"*.up.railway.app",
];

export function getPreviewOrigins(): string[] {
const origins: string[] = [];
for (let i = 1; i <= 200; i++) {
origins.push(`https://pr-${i}.osschat.dev`);
}
return origins;
}

export function getAllowedOrigins(): string[] {
return [...BASE_ALLOWED_ORIGINS, ...getPreviewOrigins()];
const siteUrl = process.env.SITE_URL;
const origins = [...ALLOWED_ORIGINS];
if (siteUrl) origins.push(siteUrl);
return origins;
}

export function getCorsOrigin(origin: string | null): string | null {
if (!origin) return null;
return getAllowedOrigins().includes(origin) ? origin : null;
const allowed = getAllowedOrigins();
for (const pattern of allowed) {
if (pattern === origin) return origin;
if (pattern.startsWith("*.")) {
const wildcardDomain = pattern.slice(1);
const rootDomain = pattern.slice(2);
try {
const url = new URL(origin);
if (
url.protocol === "https:" &&
(url.hostname.endsWith(wildcardDomain) || url.hostname === rootDomain)
) {
return origin;
}
} catch {
continue;
}
}
}
return null;
}
42 changes: 27 additions & 15 deletions apps/web/src/lib/auth-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ interface AuthContextValue {
session: { id: string; token: string } | null;
loading: boolean;
isAuthenticated: boolean;
refetchSession: () => Promise<void>;
refetchSession: () => Promise<boolean>;
}

const AuthContext = createContext<AuthContextValue | null>(null);
Expand All @@ -122,9 +122,9 @@ export function StableAuthProvider({ children }: { children: ReactNode }) {
const fetchedRef = useRef(false);
const fetchingRef = useRef(false);

const fetchSession = useCallback(async () => {
const fetchSession = useCallback(async (): Promise<boolean> => {
// Prevent concurrent fetches
if (fetchingRef.current) return;
if (fetchingRef.current) return false;
fetchingRef.current = true;

try {
Expand All @@ -143,12 +143,14 @@ export function StableAuthProvider({ children }: { children: ReactNode }) {
},
session: { id: result.data.session.id, token: result.data.session.token },
});
} else {
setSessionData({ user: null, session: null });
return true;
}
setSessionData({ user: null, session: null });
return false;
} catch (error) {
console.error("[StableAuthProvider] Failed to fetch session:", error);
setSessionData({ user: null, session: null });
return false;
} finally {
setLoading(false);
fetchingRef.current = false;
Expand Down Expand Up @@ -206,7 +208,7 @@ export function useAuth(): AuthContextValue {
session: null,
loading: true,
isAuthenticated: false,
refetchSession: async () => {},
refetchSession: async () => false,
};
}
return context;
Expand All @@ -216,29 +218,39 @@ export function useAuth(): AuthContextValue {
* Legacy hook for backward compatibility.
* Uses our stable, non-reactive session management.
*/
/**
* Sign in with GitHub OAuth
*/
export async function signInWithGitHub(callbackURL = "/") {
return authClient.signIn.social({
provider: "github",
callbackURL,
});
}

/**
* Sign in with Vercel OAuth
*/
export async function signInWithVercel(callbackURL = "/") {
return authClient.signIn.social({
provider: "vercel",
callbackURL,
});
}

/**
* Sign out
*/
export async function signInWithEmail(email: string, password: string) {
return authClient.signIn.email({
email,
password,
});
}

export async function signUpWithEmail(
email: string,
password: string,
name: string,
) {
return authClient.signUp.email({
email,
password,
name,
});
}

export async function signOut() {
return authClient.signOut({
fetchOptions: {
Expand Down
Loading
Loading