mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(site): use astro:env for server-only environment variables (#574)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
52d20bfca8
commit
10eb92e5ea
+23
-1
@@ -8,7 +8,7 @@ import react from '@astrojs/react';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import sentry from '@sentry/astro';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig, fontProviders } from 'astro/config';
|
||||
import { defineConfig, envField, fontProviders } from 'astro/config';
|
||||
import checkV8Urls from './integrations/check-v8-urls';
|
||||
import llmsMarkdown from './integrations/llms-markdown';
|
||||
import pagefind from './integrations/pagefind';
|
||||
@@ -24,6 +24,28 @@ export default defineConfig({
|
||||
site: SITE_URL,
|
||||
trailingSlash: 'never',
|
||||
adapter: netlify(),
|
||||
// Server-only secrets read at runtime (not inlined at build time).
|
||||
// All optional — the site degrades gracefully without auth/Mux configured.
|
||||
// See site/CLAUDE.md "Environment Variables" for full documentation.
|
||||
env: {
|
||||
schema: {
|
||||
// OAuth — powers the video uploader login flow
|
||||
OAUTH_CLIENT_ID: envField.string({ context: 'server', access: 'secret', optional: true }),
|
||||
OAUTH_CLIENT_SECRET: envField.string({ context: 'server', access: 'secret', optional: true }),
|
||||
OAUTH_REDIRECT_URI: envField.string({ context: 'server', access: 'secret', optional: true }),
|
||||
OAUTH_URL: envField.string({ context: 'server', access: 'secret', optional: true }),
|
||||
SESSION_COOKIE_PASSWORD: envField.string({ context: 'server', access: 'secret', optional: true }),
|
||||
MUX_API_URL: envField.string({
|
||||
context: 'server',
|
||||
access: 'secret',
|
||||
optional: true,
|
||||
default: 'https://api.mux.com',
|
||||
}),
|
||||
// Mux service account credentials — only used by the /api/health/mux endpoint
|
||||
MUX_TOKEN_ID: envField.string({ context: 'server', access: 'secret', optional: true }),
|
||||
MUX_TOKEN_SECRET: envField.string({ context: 'server', access: 'secret', optional: true }),
|
||||
},
|
||||
},
|
||||
redirects: {
|
||||
// Redirects are configured in netlify.toml
|
||||
},
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ActionError, defineAction } from 'astro:actions';
|
||||
import { OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI, OAUTH_URL } from 'astro:env/server';
|
||||
import { z } from 'astro:schema';
|
||||
import { SESSION_COOKIE_NAME } from '@/utils/auth';
|
||||
|
||||
const { OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI, OAUTH_URL, PROD } = import.meta.env;
|
||||
|
||||
export const auth = {
|
||||
/**
|
||||
* Initiates the OAuth 2.0 login flow
|
||||
@@ -39,7 +38,7 @@ export const auth = {
|
||||
// Store state in a short-lived cookie for verification in the callback
|
||||
ctx.cookies.set('state', state, {
|
||||
httpOnly: true,
|
||||
secure: PROD,
|
||||
secure: import.meta.env.PROD,
|
||||
sameSite: 'lax',
|
||||
maxAge: 600, // Expires after 10 minutes
|
||||
path: '/',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ActionError, defineAction } from 'astro:actions';
|
||||
import { MUX_API_URL, MUX_TOKEN_ID, MUX_TOKEN_SECRET } from 'astro:env/server';
|
||||
import { z } from 'astro:schema';
|
||||
import Mux from '@mux/mux-node';
|
||||
|
||||
@@ -18,13 +19,13 @@ function getMuxClient(token: string | undefined) {
|
||||
|
||||
return new Mux({
|
||||
authorizationToken: token,
|
||||
baseURL: import.meta.env.MUX_API_URL ?? 'https://api.mux.com',
|
||||
baseURL: MUX_API_URL,
|
||||
});
|
||||
}
|
||||
|
||||
function getHealthMuxClient() {
|
||||
const tokenId = process.env.MUX_TOKEN_ID || import.meta.env.MUX_TOKEN_ID;
|
||||
const tokenSecret = process.env.MUX_TOKEN_SECRET || import.meta.env.MUX_TOKEN_SECRET;
|
||||
const tokenId = MUX_TOKEN_ID;
|
||||
const tokenSecret = MUX_TOKEN_SECRET;
|
||||
|
||||
if (!tokenId || !tokenSecret) {
|
||||
throw new ActionError({
|
||||
|
||||
@@ -2,8 +2,28 @@
|
||||
|
||||
import type { APIContext } from 'astro';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { GET as callbackHandler } from '@/pages/api/auth/callback';
|
||||
import { exchangeAuthorizationCode, seal } from '@/utils/auth';
|
||||
|
||||
const env = vi.hoisted(() => ({
|
||||
OAUTH_CLIENT_ID: 'test-client-id' as string | undefined,
|
||||
OAUTH_CLIENT_SECRET: 'test-client-secret' as string | undefined,
|
||||
OAUTH_REDIRECT_URI: 'https://example.com/callback' as string | undefined,
|
||||
OAUTH_URL: 'https://auth.example.com' as string | undefined,
|
||||
}));
|
||||
|
||||
vi.mock('astro:env/server', () => ({
|
||||
get OAUTH_CLIENT_ID() {
|
||||
return env.OAUTH_CLIENT_ID;
|
||||
},
|
||||
get OAUTH_CLIENT_SECRET() {
|
||||
return env.OAUTH_CLIENT_SECRET;
|
||||
},
|
||||
get OAUTH_REDIRECT_URI() {
|
||||
return env.OAUTH_REDIRECT_URI;
|
||||
},
|
||||
get OAUTH_URL() {
|
||||
return env.OAUTH_URL;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/auth', () => ({
|
||||
exchangeAuthorizationCode: vi.fn(),
|
||||
@@ -12,6 +32,9 @@ vi.mock('@/utils/auth', () => ({
|
||||
INACTIVITY_EXPIRY: 300,
|
||||
}));
|
||||
|
||||
import { GET as callbackHandler } from '@/pages/api/auth/callback';
|
||||
import { exchangeAuthorizationCode, seal } from '@/utils/auth';
|
||||
|
||||
const mockOAuthResponse = {
|
||||
access_token: 'mock-access-token',
|
||||
refresh_token: 'mock-refresh-token',
|
||||
@@ -49,16 +72,16 @@ describe('callback endpoint', () => {
|
||||
vi.mocked(exchangeAuthorizationCode).mockResolvedValue(mockOAuthResponse);
|
||||
vi.mocked(seal).mockResolvedValue('encrypted-session-data');
|
||||
|
||||
vi.stubEnv('OAUTH_CLIENT_ID', 'test-client-id');
|
||||
vi.stubEnv('OAUTH_CLIENT_SECRET', 'test-client-secret');
|
||||
vi.stubEnv('OAUTH_REDIRECT_URI', 'https://example.com/callback');
|
||||
vi.stubEnv('OAUTH_URL', 'https://auth.example.com');
|
||||
vi.stubEnv('PROD', false);
|
||||
env.OAUTH_CLIENT_ID = 'test-client-id';
|
||||
env.OAUTH_CLIENT_SECRET = 'test-client-secret';
|
||||
env.OAUTH_REDIRECT_URI = 'https://example.com/callback';
|
||||
env.OAUTH_URL = 'https://auth.example.com';
|
||||
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
@@ -110,7 +133,7 @@ describe('callback endpoint', () => {
|
||||
'OAUTH_CLIENT_ID',
|
||||
'OAUTH_CLIENT_SECRET',
|
||||
])('should redirect to error when %s is missing', async (envVar) => {
|
||||
vi.stubEnv(envVar, undefined);
|
||||
env[envVar as keyof typeof env] = undefined;
|
||||
|
||||
const mockContext = createMockContext({ code: '123', state: 'abc', storedState: 'abc' });
|
||||
await callbackHandler(mockContext);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, OAUTH_URL } from 'astro:env/server';
|
||||
import type { APIRoute } from 'astro';
|
||||
|
||||
import { exchangeAuthorizationCode, INACTIVITY_EXPIRY, SESSION_COOKIE_NAME, seal } from '@/utils/auth';
|
||||
@@ -15,8 +16,6 @@ export const prerender = false;
|
||||
* 4. Redirects to success or error page
|
||||
*/
|
||||
export const GET: APIRoute = async ({ request, cookies, redirect }) => {
|
||||
const { OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, OAUTH_URL } = import.meta.env;
|
||||
|
||||
const url = new URL(request.url);
|
||||
const code = url.searchParams.get('code');
|
||||
const state = url.searchParams.get('state');
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import {
|
||||
OAUTH_CLIENT_ID,
|
||||
OAUTH_CLIENT_SECRET,
|
||||
OAUTH_REDIRECT_URI,
|
||||
OAUTH_URL,
|
||||
SESSION_COOKIE_PASSWORD,
|
||||
} from 'astro:env/server';
|
||||
import { sealData, unsealData } from 'iron-session';
|
||||
import { createRemoteJWKSet } from 'jose';
|
||||
|
||||
const { OAUTH_URL, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, SESSION_COOKIE_PASSWORD } = import.meta
|
||||
.env;
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user