feat: enforce per-user module access in Makelore

This commit is contained in:
2026-08-17 08:51:15 +08:00
parent f7171a471a
commit d16922f18c
12 changed files with 457 additions and 35 deletions

View File

@@ -9,6 +9,11 @@ import {
WORKS_SQUARE_ACTIVITY_SYNC_INTERVAL_MS,
WORKS_SQUARE_SESSION_IDLE_TIMEOUT_MS,
} from '../../shared/auth-session';
import {
DEFAULT_MODULE_ACCESS,
normalizeModuleAccess,
type ModuleAccess,
} from '../../shared/module-access';
export type AuthUser = {
username: string;
@@ -51,6 +56,10 @@ type MainSessionResponse = AuthActionResponse & {
session?: MainSession | null;
};
type ModuleAccessResponse = AuthActionResponse & {
moduleAccess?: unknown;
};
type RefreshSessionOptions = {
forceRefresh?: boolean;
};
@@ -69,6 +78,7 @@ type AuthState = {
/** One-release bridge for moving old Renderer-persisted refresh tokens into Main. */
legacyRefreshToken: string | null;
user: AuthUser | null;
moduleAccess: ModuleAccess;
init: () => Promise<void>;
loginWithBrowser: () => Promise<void>;
refreshSession: (options?: RefreshSessionOptions) => Promise<string | null>;
@@ -155,6 +165,7 @@ function getClearedSession() {
canRefresh: false,
legacyRefreshToken: null,
user: null,
moduleAccess: { ...DEFAULT_MODULE_ACCESS },
};
}
@@ -220,6 +231,16 @@ async function syncMainSession(session: {
}
}
async function readCurrentModuleAccess(fallback: ModuleAccess): Promise<ModuleAccess> {
try {
const response = await hostApiFetch<ModuleAccessResponse>('/api/auth/me');
if (!response.success) return fallback;
return normalizeModuleAccess(response.moduleAccess);
} catch {
return fallback;
}
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
@@ -326,7 +347,11 @@ export const useAuthStore = create<AuthState>()(
return;
}
set({ initialized: true, loading: false, error: null });
const moduleAccess = await readCurrentModuleAccess(
normalizeModuleAccess(state.moduleAccess),
);
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
set({ initialized: true, loading: false, error: null, moduleAccess });
},
loginWithBrowser: async () => {
@@ -343,6 +368,9 @@ export const useAuthStore = create<AuthState>()(
}
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
const moduleAccess = await readCurrentModuleAccess({ ...DEFAULT_MODULE_ACCESS });
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
set({
initialized: true,
loading: false,
@@ -351,6 +379,7 @@ export const useAuthStore = create<AuthState>()(
clientId: DEFAULT_CLIENT_ID,
...sessionFieldsFromMain(session),
user: createUserFromToken(response.token),
moduleAccess,
});
} catch (error) {
if (!isCurrentAuthSessionEpoch(operationEpoch)) return;
@@ -389,11 +418,15 @@ export const useAuthStore = create<AuthState>()(
throw new Error(response.error || 'Refresh failed');
}
const moduleAccess = await readCurrentModuleAccess(state.moduleAccess);
if (!isCurrentAuthSessionEpoch(operationEpoch)) return null;
set({
initialized: true,
loading: false,
error: null,
...sessionFieldsFromMain(session),
moduleAccess,
});
return session.accessToken;
} catch (error) {
@@ -589,7 +622,7 @@ export const useAuthStore = create<AuthState>()(
}),
{
name: 'niancode-auth',
version: 1,
version: 2,
migrate: (persistedState: unknown) => {
const state = persistedState && typeof persistedState === 'object'
? persistedState as Record<string, unknown>
@@ -601,6 +634,7 @@ export const useAuthStore = create<AuthState>()(
...rest,
canRefresh: state.canRefresh === true || Boolean(legacyRefreshToken),
legacyRefreshToken,
moduleAccess: normalizeModuleAccess(state.moduleAccess),
};
},
partialize: (state) => ({
@@ -613,6 +647,7 @@ export const useAuthStore = create<AuthState>()(
canRefresh: state.canRefresh,
legacyRefreshToken: state.legacyRefreshToken,
user: state.user,
moduleAccess: state.moduleAccess,
}),
},
),