P0 Critical: presentation isolation (client scoping), storage super_admin fix, template selection in worker, IMAGE_PROVIDERS list fix. P1 High: template layout management UI (delete/filter/bulk), slide-based parsing mode, LLM model listing & connection test, settings persistence to DB (Fernet encryption), logout button. P2 Polish: storage improvements (master deck files, per-client breakdown, bulk delete, hard purge, client selector), image generation error visibility (__image_error__ badge), hamster wheel loading animation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
102 lines
2.4 KiB
TypeScript
102 lines
2.4 KiB
TypeScript
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
|
|
|
export interface User {
|
|
id: string;
|
|
email: string;
|
|
displayName: string;
|
|
role: "super_admin" | "client_admin" | "user";
|
|
clientId?: string | null;
|
|
}
|
|
|
|
interface AuthState {
|
|
user: User | null;
|
|
isLoading: boolean;
|
|
isAuthenticated: boolean;
|
|
isDevMode: boolean;
|
|
}
|
|
|
|
const initialState: AuthState = {
|
|
user: null,
|
|
isLoading: true,
|
|
isAuthenticated: false,
|
|
isDevMode: false,
|
|
};
|
|
|
|
export const fetchCurrentUser = createAsyncThunk(
|
|
"auth/fetchCurrentUser",
|
|
async (_, { rejectWithValue }) => {
|
|
try {
|
|
const response = await fetch("/api/v1/auth/me");
|
|
if (response.status === 401) {
|
|
return rejectWithValue("Not authenticated");
|
|
}
|
|
if (!response.ok) {
|
|
return rejectWithValue("Failed to fetch user");
|
|
}
|
|
return await response.json();
|
|
} catch {
|
|
return rejectWithValue("Network error");
|
|
}
|
|
}
|
|
);
|
|
|
|
export const checkDevMode = createAsyncThunk(
|
|
"auth/checkDevMode",
|
|
async () => {
|
|
try {
|
|
const response = await fetch("/api/v1/auth/dev-status");
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
return data.dev_mode ?? false;
|
|
}
|
|
return false;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
);
|
|
|
|
export const logoutUser = createAsyncThunk(
|
|
"auth/logoutUser",
|
|
async () => {
|
|
await fetch("/api/v1/auth/logout", { method: "POST" });
|
|
return true;
|
|
}
|
|
);
|
|
|
|
const authSlice = createSlice({
|
|
name: "auth",
|
|
initialState,
|
|
reducers: {
|
|
logout: (state) => {
|
|
state.user = null;
|
|
state.isAuthenticated = false;
|
|
},
|
|
},
|
|
extraReducers: (builder) => {
|
|
builder
|
|
.addCase(fetchCurrentUser.pending, (state) => {
|
|
state.isLoading = true;
|
|
})
|
|
.addCase(fetchCurrentUser.fulfilled, (state, action) => {
|
|
state.user = action.payload;
|
|
state.isAuthenticated = true;
|
|
state.isLoading = false;
|
|
})
|
|
.addCase(fetchCurrentUser.rejected, (state) => {
|
|
state.user = null;
|
|
state.isAuthenticated = false;
|
|
state.isLoading = false;
|
|
})
|
|
.addCase(checkDevMode.fulfilled, (state, action) => {
|
|
state.isDevMode = action.payload;
|
|
})
|
|
.addCase(logoutUser.fulfilled, (state) => {
|
|
state.user = null;
|
|
state.isAuthenticated = false;
|
|
});
|
|
},
|
|
});
|
|
|
|
export const { logout } = authSlice.actions;
|
|
export default authSlice.reducer;
|