b37e15d59a
Phase 18 changes: OAuth/OIDC SSO (Authorization Code + PKCE): - alembic/versions/0015_add_oauth_fields.py: add oauth_sub UNIQUE column, drop NOT NULL on password_hash - src/fabledassistant/services/oauth.py: OIDC discovery (cached), build_auth_url, exchange_code, get_userinfo, find_or_create_oauth_user (sub→email auto-link→create) - src/fabledassistant/routes/auth.py: GET /api/auth/oauth/login and GET /api/auth/oauth/callback; LOCAL_AUTH_ENABLED guards on login/register; /api/auth/status now returns oauth_enabled + local_auth_enabled - src/fabledassistant/config.py: OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_SCOPES, LOCAL_AUTH_ENABLED, oidc_enabled() classmethod - src/fabledassistant/models/user.py: password_hash nullable, oauth_sub field, has_password bool in to_dict() - src/fabledassistant/services/auth.py: create_user accepts password=None + oauth_sub kwarg; authenticate returns None for OAuth-only users; add get_user_by_oauth_sub, link_oauth_sub, update_user_email - frontend: AuthStatus + User types updated; auth store exposes oauthEnabled + localAuthEnabled; LoginView shows SSO button / hides password form accordingly Email change: - PUT /api/auth/email: requires password confirmation for local-auth users, skips check for OAuth-only users; enforces email uniqueness - SettingsView.vue: new Email Address section pre-filled with current email, updates authStore.user in-place on success Docs: - docs/oauth-setup.md: step-by-step Authentik provider setup, example docker-compose env vars, account linking explanation, per-provider issuer URL table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
76 lines
1.9 KiB
TypeScript
76 lines
1.9 KiB
TypeScript
import { ref, computed } from "vue";
|
|
import { defineStore } from "pinia";
|
|
import { apiGet, apiPost } from "@/api/client";
|
|
import type { User, AuthStatus } from "@/types/auth";
|
|
|
|
export const useAuthStore = defineStore("auth", () => {
|
|
const user = ref<User | null>(null);
|
|
const loading = ref(true);
|
|
const hasUsers = ref(true);
|
|
const registrationOpen = ref(false);
|
|
const oauthEnabled = ref(false);
|
|
const localAuthEnabled = ref(true);
|
|
|
|
const isAuthenticated = computed(() => user.value !== null);
|
|
const isAdmin = computed(() => user.value?.role === "admin");
|
|
|
|
async function checkAuth() {
|
|
loading.value = true;
|
|
try {
|
|
user.value = await apiGet<User>("/api/auth/me");
|
|
} catch {
|
|
user.value = null;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function checkHasUsers() {
|
|
try {
|
|
const data = await apiGet<AuthStatus>("/api/auth/status");
|
|
hasUsers.value = data.has_users;
|
|
registrationOpen.value = data.registration_open;
|
|
oauthEnabled.value = data.oauth_enabled ?? false;
|
|
localAuthEnabled.value = data.local_auth_enabled ?? true;
|
|
} catch {
|
|
hasUsers.value = true;
|
|
registrationOpen.value = false;
|
|
oauthEnabled.value = false;
|
|
localAuthEnabled.value = true;
|
|
}
|
|
}
|
|
|
|
async function login(username: string, password: string) {
|
|
user.value = await apiPost<User>("/api/auth/login", { username, password });
|
|
}
|
|
|
|
async function register(username: string, password: string, email?: string) {
|
|
user.value = await apiPost<User>("/api/auth/register", {
|
|
username,
|
|
password,
|
|
email: email || undefined,
|
|
});
|
|
}
|
|
|
|
async function logout() {
|
|
await apiPost("/api/auth/logout", {});
|
|
user.value = null;
|
|
}
|
|
|
|
return {
|
|
user,
|
|
loading,
|
|
hasUsers,
|
|
registrationOpen,
|
|
oauthEnabled,
|
|
localAuthEnabled,
|
|
isAuthenticated,
|
|
isAdmin,
|
|
checkAuth,
|
|
checkHasUsers,
|
|
login,
|
|
register,
|
|
logout,
|
|
};
|
|
});
|