feat(admin-web/auth): support phone-or-email login and dim shell on expiry
Login form takes a phone-or-email identifier; user info now exposes phone so the shell can fall back through name → phone → email. When membership expires, the app stays on the current route but renders a minimal blocked state in the shell instead of redirecting to a dedicated page; an Axios interceptor flips the local session into the blocked state when the API returns trial/subscription errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -118,6 +118,7 @@ import {
|
||||
clearStoredSession,
|
||||
getStoredAccessToken,
|
||||
getStoredRefreshToken,
|
||||
markStoredMembershipBlocked,
|
||||
readStoredSession,
|
||||
setStoredTokens,
|
||||
} from "./session";
|
||||
@@ -271,6 +272,23 @@ export const apiClient = createApiClient({
|
||||
},
|
||||
});
|
||||
|
||||
const membershipBlockedErrors = new Set([
|
||||
"trial_plan_expired",
|
||||
"subscription_required",
|
||||
"subscription_inactive",
|
||||
]);
|
||||
|
||||
apiClient.raw.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error instanceof ApiClientError && membershipBlockedErrors.has(error.message)) {
|
||||
markStoredMembershipBlocked(error.message);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export const authApi = {
|
||||
login(payload: LoginRequest) {
|
||||
return publicClient.post<LoginResponse, LoginRequest>("/api/auth/login", payload);
|
||||
|
||||
@@ -95,6 +95,36 @@ export function setStoredUser(user: UserInfo | null): SessionSnapshot {
|
||||
return updateStoredSession({ user });
|
||||
}
|
||||
|
||||
export function markStoredMembershipBlocked(reason = "subscription_inactive"): SessionSnapshot {
|
||||
const snapshot = readStoredSession();
|
||||
if (!snapshot.user) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
const current = snapshot.user.membership;
|
||||
const nextUser: UserInfo = {
|
||||
...snapshot.user,
|
||||
membership: {
|
||||
plan_code: current?.plan_code ?? "",
|
||||
plan_name: current?.plan_name ?? "",
|
||||
status: "expired",
|
||||
blocked: true,
|
||||
blocked_reason: reason,
|
||||
start_at: current?.start_at ?? null,
|
||||
end_at: current?.end_at ?? null,
|
||||
article_generation_limit: current?.article_generation_limit ?? 0,
|
||||
article_quota_cycle: current?.article_quota_cycle ?? "lifetime",
|
||||
ai_points_monthly: current?.ai_points_monthly ?? 0,
|
||||
ai_point_base_chars: current?.ai_point_base_chars ?? 1000,
|
||||
company_limit: current?.company_limit ?? 0,
|
||||
image_storage_bytes: current?.image_storage_bytes ?? 0,
|
||||
contact_admin_on_expiry: current?.contact_admin_on_expiry ?? true,
|
||||
},
|
||||
};
|
||||
|
||||
return updateStoredSession({ user: nextUser });
|
||||
}
|
||||
|
||||
export function clearStoredSession(options: { notifyAuthExpired?: boolean } = {}): SessionSnapshot {
|
||||
if (hasStorage()) {
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
|
||||
Reference in New Issue
Block a user