Initial commit: StackPilot Phase 1 (Core)
Self-hosted Docker Compose manager. - Backend: FastAPI + docker-py + SQLite (JWT auth, file-first stacks, lifecycle, live status, WebSocket logs, docker-run converter, audit log) - Frontend: React + Vite + Tailwind (login/setup, dashboard, stacks, stack detail, Monaco editor, dark/light theme) - Deployment: docker-compose.yml, Dockerfiles, nginx reverse proxy Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
f732cb080b
59 files changed
+3677
No files matched your search
@@ -0,0 +1,78 @@
|
||||
import axios from "axios";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { TokenPair, User } from "@/types";
|
||||
|
||||
interface AuthState {
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
user: User | null;
|
||||
setTokens: (t: TokenPair) => void;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
setup: (username: string, password: string) => Promise<void>;
|
||||
refresh: () => Promise<string | null>;
|
||||
fetchMe: () => Promise<void>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
// Raw client without interceptors (avoids refresh loops).
|
||||
const raw = axios.create({ baseURL: "/" });
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
user: null,
|
||||
|
||||
setTokens: (t) =>
|
||||
set({ accessToken: t.access_token, refreshToken: t.refresh_token }),
|
||||
|
||||
login: async (username, password) => {
|
||||
const { data } = await raw.post<TokenPair>("/api/auth/login", {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
set({ accessToken: data.access_token, refreshToken: data.refresh_token });
|
||||
await get().fetchMe();
|
||||
},
|
||||
|
||||
setup: async (username, password) => {
|
||||
const { data } = await raw.post<TokenPair>("/api/auth/setup", {
|
||||
username,
|
||||
password,
|
||||
role: "admin",
|
||||
});
|
||||
set({ accessToken: data.access_token, refreshToken: data.refresh_token });
|
||||
await get().fetchMe();
|
||||
},
|
||||
|
||||
refresh: async () => {
|
||||
const rt = get().refreshToken;
|
||||
if (!rt) return null;
|
||||
try {
|
||||
const { data } = await raw.post<TokenPair>("/api/auth/refresh", {
|
||||
refresh_token: rt,
|
||||
});
|
||||
set({ accessToken: data.access_token, refreshToken: data.refresh_token });
|
||||
return data.access_token;
|
||||
} catch {
|
||||
set({ accessToken: null, refreshToken: null, user: null });
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
fetchMe: async () => {
|
||||
const token = get().accessToken;
|
||||
if (!token) return;
|
||||
const { data } = await raw.get<User>("/api/auth/me", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
set({ user: data });
|
||||
},
|
||||
|
||||
logout: () => set({ accessToken: null, refreshToken: null, user: null }),
|
||||
}),
|
||||
{ name: "stackpilot-auth" }
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
interface ThemeState {
|
||||
theme: "dark" | "light";
|
||||
toggle: () => void;
|
||||
apply: () => void;
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
theme: "dark",
|
||||
toggle: () => {
|
||||
set({ theme: get().theme === "dark" ? "light" : "dark" });
|
||||
get().apply();
|
||||
},
|
||||
apply: () => {
|
||||
const root = document.documentElement;
|
||||
if (get().theme === "dark") root.classList.add("dark");
|
||||
else root.classList.remove("dark");
|
||||
},
|
||||
}),
|
||||
{ name: "stackpilot-theme" }
|
||||
)
|
||||
);
|
||||
Reference in new issue
Block a user