feat: auth token env bypass
This commit is contained in:
parent
b2766ba063
commit
72b873845f
|
|
@ -29,3 +29,9 @@ LOCAL_AUTH_TOKEN=
|
|||
# Use `auto` to target the same host currently serving Pipeline on port 8001.
|
||||
# Example (explicit override): NEXT_PUBLIC_API_URL=https://mc.example.com
|
||||
NEXT_PUBLIC_API_URL=auto
|
||||
NEXT_PUBLIC_AUTH_MODE=local
|
||||
# Local auth token (used when NEXT_PUBLIC_AUTH_MODE=local).
|
||||
# When set: the app loads directly without showing the login screen.
|
||||
# When unset: users are prompted to enter their token on first load.
|
||||
# Must be at least 50 characters and match LOCAL_AUTH_TOKEN above.
|
||||
NEXT_PUBLIC_LOCAL_AUTH_TOKEN=
|
||||
|
|
|
|||
30
README.md
30
README.md
|
|
@ -149,7 +149,35 @@ Mission Control supports two authentication modes:
|
|||
- `local`: shared bearer token mode (default for self-hosted use)
|
||||
- `clerk`: Clerk JWT mode
|
||||
|
||||
Environment templates:
|
||||
### Local auth mode
|
||||
|
||||
Set `AUTH_MODE=local` (backend) and `NEXT_PUBLIC_AUTH_MODE=local` (frontend).
|
||||
|
||||
**Option 1 — Token in environment (recommended for self-hosted)**
|
||||
|
||||
Set `NEXT_PUBLIC_LOCAL_AUTH_TOKEN` in the frontend environment. The app loads directly with no login screen. The token is used automatically for every API call.
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_AUTH_MODE=local
|
||||
NEXT_PUBLIC_LOCAL_AUTH_TOKEN=your-bearer-token-here
|
||||
```
|
||||
|
||||
**Option 2 — Token entered on load**
|
||||
|
||||
Leave `NEXT_PUBLIC_LOCAL_AUTH_TOKEN` unset. On first load, users are presented with a login screen and must paste their bearer token before accessing the app. The token is stored in `sessionStorage` for the duration of the browser session.
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_AUTH_MODE=local
|
||||
# NEXT_PUBLIC_LOCAL_AUTH_TOKEN is not set — login screen will appear
|
||||
```
|
||||
|
||||
The bearer token must match `LOCAL_AUTH_TOKEN` set on the backend (minimum 50 characters).
|
||||
|
||||
### Clerk mode
|
||||
|
||||
Set `AUTH_MODE=clerk` and configure `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`.
|
||||
|
||||
### Environment templates
|
||||
|
||||
- Root: [`.env.example`](./.env.example)
|
||||
- Backend: [`backend/.env.example`](./backend/.env.example)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ services:
|
|||
args:
|
||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-auto}
|
||||
NEXT_PUBLIC_AUTH_MODE: ${AUTH_MODE}
|
||||
NEXT_PUBLIC_LOCAL_AUTH_TOKEN: ${LOCAL_AUTH_TOKEN}
|
||||
# Optional, user-managed env file.
|
||||
# IMPORTANT: do NOT load `.env.example` here because it contains non-empty
|
||||
# placeholder Clerk keys, which can accidentally flip Clerk "on".
|
||||
|
|
@ -66,6 +67,7 @@ services:
|
|||
environment:
|
||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-auto}
|
||||
NEXT_PUBLIC_AUTH_MODE: ${AUTH_MODE}
|
||||
NEXT_PUBLIC_LOCAL_AUTH_TOKEN: ${LOCAL_AUTH_TOKEN}
|
||||
depends_on:
|
||||
- backend
|
||||
ports:
|
||||
|
|
|
|||
|
|
@ -5,9 +5,15 @@ NEXT_PUBLIC_API_URL=auto
|
|||
|
||||
# Auth mode: clerk or local.
|
||||
# - clerk: Clerk sign-in flow
|
||||
# - local: shared bearer token entered in UI
|
||||
# - local: shared bearer token, entered in UI or pre-set via env
|
||||
NEXT_PUBLIC_AUTH_MODE=local
|
||||
|
||||
# Local auth token (used when NEXT_PUBLIC_AUTH_MODE=local).
|
||||
# When set: the app loads directly without showing the login screen.
|
||||
# When unset: users are prompted to enter their token on first load.
|
||||
# Must be at least 50 characters and match LOCAL_AUTH_TOKEN on the backend.
|
||||
NEXT_PUBLIC_LOCAL_AUTH_TOKEN=
|
||||
|
||||
# Clerk auth (used when NEXT_PUBLIC_AUTH_MODE=clerk)
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
|
||||
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/boards
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ export function isLocalAuthMode(): boolean {
|
|||
return process.env.NEXT_PUBLIC_AUTH_MODE === AuthMode.Local;
|
||||
}
|
||||
|
||||
/** Token pre-configured via env — skips the login screen when set. */
|
||||
export function getEnvToken(): string | null {
|
||||
return process.env.NEXT_PUBLIC_LOCAL_AUTH_TOKEN?.trim() || null;
|
||||
}
|
||||
|
||||
export function setLocalAuthToken(token: string): void {
|
||||
localToken = token;
|
||||
if (typeof window === "undefined") return;
|
||||
|
|
|
|||
|
|
@ -7,39 +7,9 @@ import { setLocalAuthToken } from "@/auth/localAuth";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { getApiBaseUrl } from "@/lib/api-base";
|
||||
|
||||
const LOCAL_AUTH_TOKEN_MIN_LENGTH = 50;
|
||||
|
||||
async function validateLocalToken(token: string): Promise<string | null> {
|
||||
let baseUrl: string;
|
||||
try {
|
||||
baseUrl = getApiBaseUrl();
|
||||
} catch {
|
||||
return "Unable to resolve backend URL.";
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${baseUrl}/api/v1/users/me`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return "Unable to reach backend to validate token.";
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return null;
|
||||
}
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return "Token is invalid.";
|
||||
}
|
||||
return `Unable to validate token (HTTP ${response.status}).`;
|
||||
}
|
||||
|
||||
type LocalAuthLoginProps = {
|
||||
onAuthenticated?: () => void;
|
||||
};
|
||||
|
|
@ -49,9 +19,8 @@ const defaultOnAuthenticated = () => window.location.reload();
|
|||
export function LocalAuthLogin({ onAuthenticated }: LocalAuthLoginProps) {
|
||||
const [token, setToken] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const cleaned = token.trim();
|
||||
if (!cleaned) {
|
||||
|
|
@ -64,15 +33,6 @@ export function LocalAuthLogin({ onAuthenticated }: LocalAuthLoginProps) {
|
|||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidating(true);
|
||||
const validationError = await validateLocalToken(cleaned);
|
||||
setIsValidating(false);
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalAuthToken(cleaned);
|
||||
setError(null);
|
||||
(onAuthenticated ?? defaultOnAuthenticated)();
|
||||
|
|
@ -120,7 +80,6 @@ export function LocalAuthLogin({ onAuthenticated }: LocalAuthLoginProps) {
|
|||
onChange={(event) => setToken(event.target.value)}
|
||||
placeholder="Paste token"
|
||||
autoFocus
|
||||
disabled={isValidating}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -133,13 +92,8 @@ export function LocalAuthLogin({ onAuthenticated }: LocalAuthLoginProps) {
|
|||
Token must be at least {LOCAL_AUTH_TOKEN_MIN_LENGTH} characters.
|
||||
</p>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={isValidating}
|
||||
>
|
||||
{isValidating ? "Validating..." : "Continue"}
|
||||
<Button type="submit" className="w-full" size="lg">
|
||||
Continue
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import { useEffect, type ReactNode } from "react";
|
|||
import { isLikelyValidClerkPublishableKey } from "@/auth/clerkKey";
|
||||
import {
|
||||
clearLocalAuthToken,
|
||||
getEnvToken,
|
||||
getLocalAuthToken,
|
||||
isLocalAuthMode,
|
||||
setLocalAuthToken,
|
||||
} from "@/auth/localAuth";
|
||||
import { LocalAuthLogin } from "@/components/organisms/LocalAuthLogin";
|
||||
|
||||
|
|
@ -21,6 +23,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
}, [localMode]);
|
||||
|
||||
if (localMode) {
|
||||
// If a token is pre-set via NEXT_PUBLIC_LOCAL_AUTH_TOKEN, use it and skip the login screen.
|
||||
const envToken = getEnvToken();
|
||||
if (envToken) {
|
||||
setLocalAuthToken(envToken);
|
||||
return <>{children}</>;
|
||||
}
|
||||
if (!getLocalAuthToken()) {
|
||||
return <LocalAuthLogin />;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue