Initial open-source release (MIT): image2api AI gateway

Full Go backend + Vue 3 frontend, OpenAI-compatible API, multi-provider
account pools, billing/admin, Docker one-command deploy with auto HTTPS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 22:59:04 +08:00
co-authored by Claude Opus 4.8
commit 606caaf047
142 changed files with 33648 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
// Client-side auth state. The session token lives in localStorage and is sent
// as `Authorization: Bearer <token>` on every admin API call (see api.js).
// The server slides the 24h session whenever it's used with <22h left, so an
// active admin never gets logged out; we also re-validate via /me on a timer.
import { reactive } from 'vue'
const TOKEN_KEY = 'gw_token'
const BASE = import.meta.env.VITE_API_BASE || ''
export const auth = reactive({
token: localStorage.getItem(TOKEN_KEY) || '',
user: null, // { id, email, name, role, status, credits, invite_code, ... }
ready: false, // true once an initial /me check has resolved
loginOpen: false, // is the login modal showing?
loginIntent: '', // where to go after a successful login
startMode: 'login',// which tab the modal opens on: 'login' | 'register'
pendingInvite: '', // invite code from a /?ref=CODE link, sent with register
})
export function isAuthed() { return !!auth.token && !!auth.user }
export function isAdmin() { return isAuthed() && auth.user.role === 'admin' }
export function isAgent() { return isAuthed() && auth.user.role === 'agent' }
export function getToken() { return auth.token }
/** Open the login modal, remembering where the user wanted to go. */
export function openLogin(intent = '') { auth.loginIntent = intent || ''; auth.startMode = 'login'; auth.loginOpen = true }
/** Open straight to the register tab, carrying an optional invite code.
* Default landing is the home page — an invitee clicking a /?ref=CODE
* link should NOT be punted into the画图 flow before they've explored. */
export function openRegister(inviteCode = '', intent = '/') {
auth.pendingInvite = inviteCode || ''
auth.loginIntent = intent || ''
auth.startMode = 'register'
auth.loginOpen = true
}
export function closeLogin() { auth.loginOpen = false }
export function setSession(token, user) {
auth.token = token || ''
auth.user = user || null
if (token) localStorage.setItem(TOKEN_KEY, token)
else localStorage.removeItem(TOKEN_KEY)
}
export function clearSession() {
auth.token = ''
auth.user = null
localStorage.removeItem(TOKEN_KEY)
}
/** Validate the stored token against /me. Refreshes auth.user; clears on 401.
* Returns the user (or null). Hitting /me also slides the server session. */
export async function refreshMe() {
if (!auth.token) { auth.user = null; auth.ready = true; return null }
try {
const r = await fetch(`${BASE}/admin/api/auth/me`, {
headers: { Authorization: `Bearer ${auth.token}` },
})
if (r.ok) {
const d = await r.json()
auth.user = d.user
} else {
clearSession()
}
} catch {
// network error — keep the token, don't force a logout
}
auth.ready = true
return auth.user
}
export async function logout() {
if (auth.token) {
try {
await fetch(`${BASE}/admin/api/auth/logout`, {
method: 'POST',
headers: { Authorization: `Bearer ${auth.token}` },
})
} catch { /* ignore */ }
}
clearSession()
}
// Keep the session warm: re-validate every 10 minutes while a tab is open.
setInterval(() => { if (auth.token) refreshMe() }, 10 * 60 * 1000)