Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files
.gitignore
CHANGED
|
@@ -1,3 +1,12 @@
|
|
| 1 |
examples
|
| 2 |
output
|
| 3 |
-
.venv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
examples
|
| 2 |
output
|
| 3 |
+
.venv
|
| 4 |
+
venv
|
| 5 |
+
.env
|
| 6 |
+
.env.*
|
| 7 |
+
!.env.example
|
| 8 |
+
__pycache__/
|
| 9 |
+
*.py[cod]
|
| 10 |
+
*.egg-info/
|
| 11 |
+
.pytest_cache/
|
| 12 |
+
.DS_Store
|
docs/WEB_DEV_GUIDE.md
CHANGED
|
@@ -31,6 +31,7 @@ Do not commit secrets; use a `.env` with your process manager if you like.
|
|
| 31 |
| `WEB_UI_PASSWORD` | Login password. |
|
| 32 |
| `SESSION_SECRET` | Session signing, e.g. `openssl rand -hex 32`. |
|
| 33 |
| `GENERATION_OPTIONS_PATH` | Optional. Overrides default `web/config/generation_options.json`. |
|
|
|
|
| 34 |
|
| 35 |
```bash
|
| 36 |
export GEMINI_API_KEY="your_key"
|
|
|
|
| 31 |
| `WEB_UI_PASSWORD` | Login password. |
|
| 32 |
| `SESSION_SECRET` | Session signing, e.g. `openssl rand -hex 32`. |
|
| 33 |
| `GENERATION_OPTIONS_PATH` | Optional. Overrides default `web/config/generation_options.json`. |
|
| 34 |
+
| `SESSION_COOKIE_SECURE` | Optional. Set to `1` / `true` / `yes` to send session cookies with the **Secure** flag (use on real HTTPS, e.g. Hugging Face). Leave unset for `http://127.0.0.1` local dev. Requires proxy to set `X-Forwarded-Proto` (see §10). |
|
| 35 |
|
| 36 |
```bash
|
| 37 |
export GEMINI_API_KEY="your_key"
|
web/backend/main.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
"""FastAPI application: session auth, Gemini generation API, static SPA."""
|
| 2 |
|
|
|
|
| 3 |
from contextlib import asynccontextmanager
|
| 4 |
from pathlib import Path
|
| 5 |
|
|
@@ -22,12 +23,21 @@ async def lifespan(app: FastAPI):
|
|
| 22 |
|
| 23 |
app = FastAPI(title="Gemini Studio Web", lifespan=lifespan)
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
app.add_middleware(
|
| 26 |
SessionMiddleware,
|
| 27 |
secret_key=get_session_secret(),
|
| 28 |
max_age=14 * 24 * 3600,
|
| 29 |
same_site="lax",
|
| 30 |
-
https_only=
|
| 31 |
)
|
| 32 |
|
| 33 |
app.include_router(auth.router)
|
|
|
|
| 1 |
"""FastAPI application: session auth, Gemini generation API, static SPA."""
|
| 2 |
|
| 3 |
+
import os
|
| 4 |
from contextlib import asynccontextmanager
|
| 5 |
from pathlib import Path
|
| 6 |
|
|
|
|
| 23 |
|
| 24 |
app = FastAPI(title="Gemini Studio Web", lifespan=lifespan)
|
| 25 |
|
| 26 |
+
# Session cookie: default allows http://127.0.0.1 local dev. For HTTPS-only deployments
|
| 27 |
+
# where the proxy sets X-Forwarded-Proto (see uvicorn --forwarded-allow-ips), set
|
| 28 |
+
# SESSION_COOKIE_SECURE=1 so browsers get the Secure flag (recommended on HF Spaces).
|
| 29 |
+
_session_secure = os.environ.get("SESSION_COOKIE_SECURE", "").strip().lower() in (
|
| 30 |
+
"1",
|
| 31 |
+
"true",
|
| 32 |
+
"yes",
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
app.add_middleware(
|
| 36 |
SessionMiddleware,
|
| 37 |
secret_key=get_session_secret(),
|
| 38 |
max_age=14 * 24 * 3600,
|
| 39 |
same_site="lax",
|
| 40 |
+
https_only=_session_secure,
|
| 41 |
)
|
| 42 |
|
| 43 |
app.include_router(auth.router)
|
web/backend/static/assets/index-Cxt2XIBW.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
web/backend/static/index.html
CHANGED
|
@@ -43,7 +43,7 @@
|
|
| 43 |
}
|
| 44 |
})();
|
| 45 |
</script>
|
| 46 |
-
<script type="module" crossorigin src="/assets/index-
|
| 47 |
<link rel="stylesheet" crossorigin href="/assets/index-BIkuBov8.css">
|
| 48 |
</head>
|
| 49 |
<body>
|
|
|
|
| 43 |
}
|
| 44 |
})();
|
| 45 |
</script>
|
| 46 |
+
<script type="module" crossorigin src="/assets/index-Cxt2XIBW.js"></script>
|
| 47 |
<link rel="stylesheet" crossorigin href="/assets/index-BIkuBov8.css">
|
| 48 |
</head>
|
| 49 |
<body>
|
web/frontend/src/context/GenerationOptionsContext.tsx
CHANGED
|
@@ -48,6 +48,10 @@ export function GenerationOptionsProvider({ children }: { children: ReactNode })
|
|
| 48 |
let cancelled = false;
|
| 49 |
fetch("/api/config/generation-options", { credentials: "include" })
|
| 50 |
.then(async (r) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
const j = await r.json().catch(() => ({}));
|
| 52 |
if (!r.ok) {
|
| 53 |
const detail = (j as { detail?: unknown }).detail;
|
|
@@ -62,7 +66,8 @@ export function GenerationOptionsProvider({ children }: { children: ReactNode })
|
|
| 62 |
return j as GenerationOptions;
|
| 63 |
})
|
| 64 |
.then((opts) => {
|
| 65 |
-
if (
|
|
|
|
| 66 |
})
|
| 67 |
.catch((e: unknown) => {
|
| 68 |
if (!cancelled)
|
|
|
|
| 48 |
let cancelled = false;
|
| 49 |
fetch("/api/config/generation-options", { credentials: "include" })
|
| 50 |
.then(async (r) => {
|
| 51 |
+
if (r.status === 401) {
|
| 52 |
+
window.location.assign("/login");
|
| 53 |
+
return null;
|
| 54 |
+
}
|
| 55 |
const j = await r.json().catch(() => ({}));
|
| 56 |
if (!r.ok) {
|
| 57 |
const detail = (j as { detail?: unknown }).detail;
|
|
|
|
| 66 |
return j as GenerationOptions;
|
| 67 |
})
|
| 68 |
.then((opts) => {
|
| 69 |
+
if (cancelled || opts === null) return;
|
| 70 |
+
setData(opts);
|
| 71 |
})
|
| 72 |
.catch((e: unknown) => {
|
| 73 |
if (!cancelled)
|
web/frontend/src/pages/Login.tsx
CHANGED
|
@@ -1,13 +1,23 @@
|
|
| 1 |
-
import { FormEvent, useState } from "react";
|
| 2 |
import { useNavigate } from "react-router-dom";
|
| 3 |
-
import { login } from "../api";
|
| 4 |
|
| 5 |
export function Login() {
|
| 6 |
const [password, setPassword] = useState("");
|
| 7 |
const [err, setErr] = useState<string | null>(null);
|
| 8 |
const [loading, setLoading] = useState(false);
|
|
|
|
| 9 |
const nav = useNavigate();
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
async function onSubmit(e: FormEvent) {
|
| 12 |
e.preventDefault();
|
| 13 |
setErr(null);
|
|
@@ -23,6 +33,14 @@ export function Login() {
|
|
| 23 |
}
|
| 24 |
}
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
return (
|
| 27 |
<div className="min-h-screen flex items-center justify-center p-6 sm:p-10 bg-gradient-to-br from-paper via-indigo-50/35 to-violet-100/45">
|
| 28 |
<div className="w-full max-w-md rounded-2xl border border-line/80 bg-white/85 backdrop-blur-sm shadow-xl shadow-indigo-950/10 p-10 sm:p-12">
|
|
|
|
| 1 |
+
import { FormEvent, useEffect, useState } from "react";
|
| 2 |
import { useNavigate } from "react-router-dom";
|
| 3 |
+
import { fetchMe, login } from "../api";
|
| 4 |
|
| 5 |
export function Login() {
|
| 6 |
const [password, setPassword] = useState("");
|
| 7 |
const [err, setErr] = useState<string | null>(null);
|
| 8 |
const [loading, setLoading] = useState(false);
|
| 9 |
+
const [checkingSession, setCheckingSession] = useState(true);
|
| 10 |
const nav = useNavigate();
|
| 11 |
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
fetchMe()
|
| 14 |
+
.then((r) => {
|
| 15 |
+
if (r.authenticated) nav("/image", { replace: true });
|
| 16 |
+
})
|
| 17 |
+
.catch(() => {})
|
| 18 |
+
.finally(() => setCheckingSession(false));
|
| 19 |
+
}, [nav]);
|
| 20 |
+
|
| 21 |
async function onSubmit(e: FormEvent) {
|
| 22 |
e.preventDefault();
|
| 23 |
setErr(null);
|
|
|
|
| 33 |
}
|
| 34 |
}
|
| 35 |
|
| 36 |
+
if (checkingSession) {
|
| 37 |
+
return (
|
| 38 |
+
<div className="min-h-screen flex items-center justify-center text-mist text-sm">
|
| 39 |
+
请稍候…
|
| 40 |
+
</div>
|
| 41 |
+
);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
return (
|
| 45 |
<div className="min-h-screen flex items-center justify-center p-6 sm:p-10 bg-gradient-to-br from-paper via-indigo-50/35 to-violet-100/45">
|
| 46 |
<div className="w-full max-w-md rounded-2xl border border-line/80 bg-white/85 backdrop-blur-sm shadow-xl shadow-indigo-950/10 p-10 sm:p-12">
|