nuxt-supabase-team-auth
Version:
Drop-in Nuxt 3 module for team-based authentication with Supabase
1,021 lines (1,020 loc) • 37.1 kB
JavaScript
import { ref, computed, triggerRef } from "vue";
import { $fetch } from "ofetch";
import { useSessionSync } from "./useSessionSync.js";
import { useSession } from "./useSession.js";
import { useSupabaseClient } from "./useSupabaseComposables.js";
import { useToast, useState } from "#imports";
function getErrorForLogging(error) {
if (error && typeof error === "object") {
return error;
}
return { message: String(error) };
}
async function createAuthHeaders(supabaseClient) {
const headers = {
"Content-Type": "application/json"
};
try {
if (supabaseClient) {
const { data: { session }, error } = await supabaseClient.auth.getSession();
if (!error && session?.access_token) {
headers["Authorization"] = `Bearer ${session.access_token}`;
}
} else {
const { getSession } = useSession();
const session = await getSession();
if (session?.access_token) {
headers["Authorization"] = `Bearer ${session.access_token}`;
}
}
} catch (error) {
console.warn("Failed to get session for auth headers:", error);
}
return headers;
}
let authListenerRegistered = false;
let cachedClient = null;
export function resetTeamAuthState() {
cachedClient = null;
}
const IMPERSONATION_STORAGE_KEY = "team_auth_impersonation";
const loadImpersonationFromStorage = () => {
if (!import.meta.client) return {};
try {
const stored = localStorage.getItem(IMPERSONATION_STORAGE_KEY);
if (stored) {
const data = JSON.parse(stored);
if (new Date(data.expiresAt) > /* @__PURE__ */ new Date()) {
return {
impersonating: true,
impersonatedUser: data.targetUser,
impersonationExpiresAt: new Date(data.expiresAt),
impersonationSessionId: data.sessionId
};
} else {
localStorage.removeItem(IMPERSONATION_STORAGE_KEY);
}
}
} catch (error) {
console.error("Failed to load impersonation data:", getErrorForLogging(error));
localStorage.removeItem(IMPERSONATION_STORAGE_KEY);
}
return {};
};
const saveImpersonationToStorage = (data) => {
if (!import.meta.client) return;
try {
const storageData = {
sessionId: data.impersonationSessionId,
targetUser: data.impersonatedUser,
expiresAt: data.impersonationExpiresAt?.toISOString()
};
localStorage.setItem(IMPERSONATION_STORAGE_KEY, JSON.stringify(storageData));
} catch (error) {
console.error("Failed to save impersonation data:", getErrorForLogging(error));
}
};
const createInitialAuthState = () => {
const impersonationState = loadImpersonationFromStorage();
return {
// Core auth
user: null,
profile: null,
team: null,
role: null,
teamMembers: [],
// Impersonation state (unified here) - restored from localStorage
impersonating: impersonationState.impersonating || false,
impersonatedUser: impersonationState.impersonatedUser || null,
impersonationExpiresAt: impersonationState.impersonationExpiresAt || null,
originalUser: null,
// Store the super admin
impersonationSessionId: impersonationState.impersonationSessionId || null,
justStartedImpersonation: false,
// UI flag for modal dismissal
stoppingImpersonation: false,
// Flag to indicate stopping in progress
// State management
loading: true,
initialized: false
};
};
export function useTeamAuth(injectedClient) {
const authState = useState("team-auth", () => createInitialAuthState());
const updateAuthState = (updates) => {
authState.value = { ...authState.value, ...updates };
if ("impersonating" in updates || "impersonatedUser" in updates || "impersonationSessionId" in updates) {
if (authState.value.impersonating) {
saveImpersonationToStorage(authState.value);
} else {
if (import.meta.client) {
localStorage.removeItem(IMPERSONATION_STORAGE_KEY);
}
}
}
};
const currentUser = computed(() => authState.value.user);
const currentProfile = computed(() => authState.value.profile);
const currentTeam = computed(() => authState.value.team);
const currentRole = computed(() => authState.value.role);
const teamMembers = computed(() => authState.value.teamMembers);
const isLoading = computed(() => authState.value.loading);
const isImpersonating = computed(() => authState.value.impersonating);
const impersonationExpiresAt = computed(() => authState.value.impersonationExpiresAt);
const impersonatedUser = computed(() => authState.value.impersonatedUser);
const originalUser = computed(() => authState.value.originalUser);
const justStartedImpersonation = computed(() => authState.value.justStartedImpersonation);
const sessionSync = useSessionSync();
const getSupabaseClient = () => {
if (injectedClient) return injectedClient;
return useSupabaseClient();
};
const getClient = () => {
if (import.meta.server) {
throw new Error("Supabase client not available during SSR");
}
if (!cachedClient) {
cachedClient = getSupabaseClient();
}
return cachedClient;
};
const updateCompleteAuthState = async (user) => {
try {
if (authState.value.impersonating && authState.value.impersonatedUser) {
const impersonatedData = authState.value.impersonatedUser;
updateAuthState({
user: {
id: user.id,
email: user.email,
user_metadata: user.user_metadata
},
profile: {
id: impersonatedData.id,
full_name: impersonatedData.full_name,
email: impersonatedData.email
},
team: impersonatedData.team ? {
id: impersonatedData.team.id,
name: impersonatedData.team.name
} : null,
role: impersonatedData.role,
loading: false
});
return;
}
if (authState.value.stoppingImpersonation) {
updateAuthState({
user: {
id: user.id,
email: user.email,
user_metadata: user.user_metadata
},
profile: null,
// Will be populated by background refresh if needed
team: null,
// Will be populated by background refresh if needed
role: null,
// Will be populated by background refresh if needed
stoppingImpersonation: false,
// Clear the flag
loading: false
});
return;
}
try {
const [profileResult, teamResult] = await Promise.all([
getClient().from("profiles").select("*").eq("id", user.id).single(),
getClient().from("team_members").select(`
role,
teams!inner (
id, name, created_at, company_name,
company_address_line1, company_address_line2,
company_city, company_state, company_postal_code,
company_country, company_vat_number
)
`).eq("user_id", user.id).single()
]);
updateAuthState({
user: {
id: user.id,
email: user.email,
user_metadata: user.user_metadata
},
profile: profileResult.data || null,
team: teamResult.data ? {
id: teamResult.data.teams.id,
name: teamResult.data.teams.name,
created_at: teamResult.data.teams.created_at,
company_name: teamResult.data.teams.company_name,
company_address_line1: teamResult.data.teams.company_address_line1,
company_address_line2: teamResult.data.teams.company_address_line2,
company_city: teamResult.data.teams.company_city,
company_state: teamResult.data.teams.company_state,
company_postal_code: teamResult.data.teams.company_postal_code,
company_country: teamResult.data.teams.company_country,
company_vat_number: teamResult.data.teams.company_vat_number
} : null,
role: teamResult.data?.role || null,
loading: false
});
} catch (error) {
console.error("Failed to update auth state:", getErrorForLogging(error));
updateAuthState({
user: {
id: user.id,
email: user.email,
user_metadata: user.user_metadata
},
profile: null,
team: null,
role: null,
loading: false
});
}
} catch (outerError) {
console.error("Auth state update failed:", getErrorForLogging(outerError));
}
};
const resetAuthState = () => {
authState.value = {
...authState.value,
user: null,
profile: null,
team: null,
role: null,
impersonating: false,
impersonationExpiresAt: null,
loading: false
};
};
const lastProcessedEvent = ref("");
const initializeAuth = async () => {
if (authState.value.initialized) {
return;
}
try {
const { data: { session } } = await getClient().auth.getSession();
if (session?.user) {
await updateCompleteAuthState(session.user);
} else {
if (authState.value.impersonating) {
if (import.meta.client) {
localStorage.removeItem(IMPERSONATION_STORAGE_KEY);
}
authState.value = {
...createInitialAuthState(),
impersonating: false,
impersonatedUser: null,
impersonationExpiresAt: null,
impersonationSessionId: null,
loading: false,
initialized: false
};
} else {
authState.value = { ...authState.value, loading: false };
}
}
if (!authListenerRegistered) {
authListenerRegistered = true;
getClient().auth.onAuthStateChange(async (event, session2) => {
const eventKey = `${event}:${session2?.user?.id || "none"}:${session2?.user?.email || "none"}`;
if (lastProcessedEvent.value === eventKey) {
return;
}
lastProcessedEvent.value = eventKey;
switch (event) {
case "SIGNED_IN":
case "TOKEN_REFRESHED":
case "USER_UPDATED":
if (session2?.user) {
await updateCompleteAuthState(session2.user);
}
break;
case "SIGNED_OUT":
resetAuthState();
lastProcessedEvent.value = "";
break;
}
});
}
authState.value.initialized = true;
} catch (error) {
console.error("Auth initialization failed:", getErrorForLogging(error));
authState.value = { ...authState.value, loading: false };
}
};
if (import.meta.client && !authState.value.initialized) {
initializeAuth();
}
return {
// State
currentUser,
currentProfile,
currentTeam,
currentRole,
teamMembers,
isLoading,
isImpersonating,
impersonationExpiresAt,
impersonatedUser,
originalUser,
justStartedImpersonation,
// Authentication methods
signUpWithTeam: async (email, password, teamName) => {
try {
authState.value = { ...authState.value, loading: true };
const response = await $fetch("/api/signup-with-team", {
method: "POST",
body: {
email,
password,
teamName
}
});
if (!response.success) {
throw { code: "SIGNUP_FAILED", message: response.message || "Signup failed" };
}
const { error: signInError } = await getClient().auth.signInWithPassword({
email,
password
});
if (signInError) {
throw { code: "SIGNIN_AFTER_SIGNUP_FAILED", message: signInError.message };
}
} catch (error) {
console.error("Sign up with team failed:", getErrorForLogging(error));
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
signIn: async (email, password) => {
try {
authState.value = { ...authState.value, loading: true };
const { data: _data, error } = await getClient().auth.signInWithPassword({
email,
password
});
if (error) {
throw { code: "SIGNIN_FAILED", message: error.message };
}
} catch (error) {
console.error("Sign in failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
signOut: async () => {
try {
authState.value = { ...authState.value, loading: true };
await getClient().auth.signOut();
} catch (error) {
console.error("Sign out failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
// Profile methods
getProfile: async () => {
if (!currentUser.value) return null;
const { data, error } = await getClient().from("profiles").select("*").eq("id", currentUser.value.id).single();
if (error) {
console.error("Failed to fetch profile:", error);
return null;
}
return data;
},
updateProfile: async (updates) => {
if (!currentUser.value) {
throw new Error("No authenticated user");
}
try {
authState.value = { ...authState.value, loading: true };
if ("password" in updates && updates.password) {
const { error: passwordError } = await getClient().auth.updateUser({
password: updates.password
});
if (passwordError) {
throw { code: "PASSWORD_UPDATE_FAILED", message: passwordError.message };
}
const { password: _, ...profileUpdates } = updates;
updates = profileUpdates;
}
if (Object.keys(updates).length > 0) {
const { data, error } = await getClient().from("profiles").update(updates).eq("id", currentUser.value.id).select().single();
if (error) {
throw { code: "PROFILE_UPDATE_FAILED", message: error.message };
}
authState.value = {
...authState.value,
profile: { ...authState.value.profile, ...data }
};
triggerRef(authState);
}
} catch (error) {
console.error("Profile update failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
// Team management methods
inviteMember: async (email, role = "member") => {
if (!currentTeam.value) {
throw new Error("No current team available");
}
const validRoles = ["member", "admin"];
if (!validRoles.includes(role)) {
throw new Error("Invalid role. Must be member or admin");
}
if (!currentTeam.value?.id) {
throw new Error("No team selected");
}
if (!currentRole.value || !["admin", "owner", "super_admin"].includes(currentRole.value)) {
throw new Error("You do not have permission to invite members");
}
try {
authState.value = { ...authState.value, loading: true };
const headers = {
"Content-Type": "application/json"
};
try {
const { data: { session }, error: sessionError } = await getClient().auth.getSession();
if (sessionError) {
console.warn("Session error:", sessionError);
} else if (session?.access_token) {
headers["Authorization"] = `Bearer ${session.access_token}`;
}
} catch (error) {
console.warn("Failed to get session for auth headers:", error);
}
const response = await $fetch("/api/invite-member", {
method: "POST",
headers,
body: {
email,
role,
teamId: currentTeam.value.id
}
});
if (!response.success) {
throw { code: "INVITE_FAILED", message: response.message || "Failed to send invite" };
}
} catch (error) {
console.error("Invite member failed:", error);
let errorMessage = "Failed to send invitation";
if (error?.data?.message) {
errorMessage = error.data.message;
} else if (error?.statusMessage) {
errorMessage = error.statusMessage;
} else if (error?.message) {
errorMessage = error.message;
} else if (error?.data?.error) {
errorMessage = error.data.error;
}
throw new Error(errorMessage);
} finally {
authState.value = { ...authState.value, loading: false };
}
},
getPendingInvitations: async () => {
if (!currentRole.value || !["owner", "admin", "super_admin"].includes(currentRole.value)) {
throw new Error("You do not have permission to view pending invitations");
}
if (!currentTeam.value) {
throw new Error("No current team available");
}
try {
authState.value = { ...authState.value, loading: true };
const headers = await createAuthHeaders(getClient());
const response = await $fetch("/api/get-pending-invitations", {
method: "POST",
headers,
body: {
teamId: currentTeam.value.id
}
});
if (!response.success) {
throw { code: "GET_INVITATIONS_FAILED", message: response.message || "Failed to fetch invitations" };
}
return response.invitations || [];
} catch (error) {
console.error("Get pending invitations failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
revokeInvite: async (userId) => {
if (!currentTeam.value) {
throw new Error("No current team available");
}
if (!currentRole.value || !["admin", "owner", "super_admin"].includes(currentRole.value)) {
throw new Error("You do not have permission to revoke invites");
}
try {
authState.value = { ...authState.value, loading: true };
const headers = await createAuthHeaders(getClient());
const response = await $fetch("/api/revoke-invitation", {
method: "POST",
headers,
body: {
userId,
teamId: currentTeam.value.id
}
});
if (!response.success) {
throw { code: "REVOKE_INVITE_FAILED", message: response.message || "Failed to revoke invitation" };
}
} catch (error) {
console.error("Revoke invite failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
resendInvite: async (userId) => {
if (!currentTeam.value) {
throw new Error("No current team available");
}
try {
authState.value = { ...authState.value, loading: true };
const pendingInvitations = await this.getPendingInvitations();
const invitation = pendingInvitations.find((inv) => inv.id === userId);
if (!invitation) {
throw { code: "INVITE_NOT_FOUND", message: "Invitation not found" };
}
await this.revokeInvite(userId);
const role = invitation.user_metadata?.team_role || "member";
await this.inviteMember(invitation.email, role);
} catch (error) {
console.error("Resend invite failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
promote: async (userId) => {
if (!currentRole.value || currentRole.value !== "owner") {
throw new Error("Only team owners can promote members");
}
await this.updateMemberRole(userId, "admin");
},
demote: async (userId) => {
if (!currentRole.value || !["admin", "owner"].includes(currentRole.value)) {
throw new Error("You do not have permission to demote members");
}
await this.updateMemberRole(userId, "member");
},
transferOwnership: async (userId) => {
if (!currentTeam.value || !currentUser.value) {
throw new Error("No current team or user available");
}
if (currentRole.value !== "owner") {
throw new Error("Only the current owner can transfer ownership");
}
try {
authState.value = { ...authState.value, loading: true };
const headers = await createAuthHeaders(getClient());
const response = await $fetch("/api/transfer-ownership", {
method: "POST",
headers,
body: {
teamId: currentTeam.value.id,
newOwnerId: userId
}
});
if (!response.success) {
throw { code: "TRANSFER_FAILED", message: response.message || "Failed to transfer ownership" };
}
authState.value = {
...authState.value,
role: "admin"
};
await this.getTeamMembers();
} catch (error) {
console.error("Transfer ownership failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
renameTeam: async (name) => {
if (!currentTeam.value) {
throw new Error("No current team available");
}
if (currentRole.value !== "owner") {
throw new Error("Only team owners can rename the team");
}
try {
authState.value = { ...authState.value, loading: true };
const { data: _data, error } = await getClient().from("teams").update({ name }).eq("id", currentTeam.value.id).select().single();
if (error) {
throw { code: "RENAME_TEAM_FAILED", message: error.message };
}
authState.value = {
...authState.value,
team: { ...authState.value.team, name }
};
} catch (error) {
console.error("Rename team failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
updateTeam: async (updates) => {
if (!currentTeam.value) {
throw new Error("No current team available");
}
if (currentRole.value !== "owner") {
throw new Error("Only team owners can update team settings");
}
try {
authState.value = { ...authState.value, loading: true };
const { data, error } = await getClient().from("teams").update(updates).eq("id", currentTeam.value.id).select().single();
if (error) {
throw { code: "UPDATE_TEAM_FAILED", message: error.message };
}
authState.value = {
...authState.value,
team: { ...authState.value.team, ...data }
};
} catch (error) {
console.error("Update team failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
deleteTeam: async () => {
if (!currentTeam.value) {
throw new Error("No current team available");
}
if (currentRole.value !== "owner") {
throw new Error("Only team owners can delete the team");
}
try {
authState.value = { ...authState.value, loading: true };
const headers = await createAuthHeaders();
const response = await fetch(`${process.env.SUPABASE_URL || import.meta.env.VITE_SUPABASE_URL}/functions/v1/delete-team`, {
method: "POST",
headers,
body: JSON.stringify({
team_id: currentTeam.value.id,
confirm_deletion: true
})
});
if (!response.ok) {
const errorData = await response.json();
throw {
code: errorData.error || "DELETE_TEAM_FAILED",
message: errorData.message || `HTTP ${response.status}: ${response.statusText}`
};
}
await response.json();
authState.value = {
...authState.value,
team: null,
role: null,
teamMembers: []
};
} catch (error) {
console.error("Delete team failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
// Team member methods
getTeamMembers: async () => {
if (!currentTeam.value) {
throw new Error("No current team available");
}
const { data: session } = await getClient().auth.getSession();
if (!session.session) {
throw new Error("No active session - please log in");
}
const { data: members, error } = await getClient().from("team_members").select(`
user_id,
role,
joined_at,
profiles!inner (
id,
full_name,
email
)
`).eq("team_id", currentTeam.value.id);
if (error) {
throw new Error(`Failed to load team members: ${error.message}`);
}
const mappedMembers = (members || []).map((member) => ({
id: member.user_id,
user_id: member.user_id,
role: member.role,
joined_at: member.joined_at,
user: {
email: member.profiles.email
},
profile: member.profiles
}));
authState.value = { ...authState.value, teamMembers: mappedMembers };
return mappedMembers;
},
updateMemberRole: async (userId, newRole) => {
if (!currentTeam.value) {
throw new Error("No current team available");
}
const validRoles = ["member", "admin", "owner"];
if (!validRoles.includes(newRole)) {
throw new Error("Invalid role");
}
if (!currentRole.value || !["admin", "owner", "super_admin"].includes(currentRole.value)) {
throw new Error("You do not have permission to update member roles");
}
if (newRole === "admin" && currentRole.value !== "owner") {
throw new Error("Only owners can promote members to admin");
}
if (newRole === "owner") {
throw new Error("Use transferOwnership method to change team ownership");
}
try {
authState.value = { ...authState.value, loading: true };
const { error } = await getClient().from("team_members").update({ role: newRole }).eq("team_id", currentTeam.value.id).eq("user_id", userId);
if (error) {
throw { code: "UPDATE_ROLE_FAILED", message: error.message };
}
const updatedMembers = authState.value.teamMembers.map(
(member) => member.user_id === userId ? { ...member, role: newRole } : member
);
authState.value = {
...authState.value,
teamMembers: updatedMembers
};
} catch (error) {
console.error("Update member role failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
removeMember: async (userId) => {
if (!currentTeam.value) {
throw new Error("No current team available");
}
if (!currentRole.value || !["admin", "owner", "super_admin"].includes(currentRole.value)) {
throw new Error("You do not have permission to remove members");
}
if (userId === currentUser.value?.id && currentRole.value === "owner") {
throw new Error("Team owner cannot remove themselves. Transfer ownership first.");
}
try {
authState.value = { ...authState.value, loading: true };
const headers = await createAuthHeaders();
const response = await $fetch("/api/delete-user", {
method: "POST",
headers,
body: { userId }
});
if (!response.success) {
throw { code: "DELETE_USER_FAILED", message: response.error || "Failed to delete user" };
}
const updatedMembers = authState.value.teamMembers.filter((member) => member.user_id !== userId);
authState.value = {
...authState.value,
teamMembers: updatedMembers
};
} catch (error) {
console.error("Remove member failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
getTeamMemberProfile: async (userId) => {
if (!currentRole.value || !["admin", "owner", "super_admin"].includes(currentRole.value)) {
throw new Error("You do not have permission to view member profiles");
}
const { data, error } = await getClient().from("profiles").select("*").eq("id", userId).single();
if (error) {
console.error("Failed to fetch team member profile:", error);
return null;
}
return data;
},
updateTeamMemberProfile: async (userId, updates) => {
if (!currentRole.value || !["admin", "owner", "super_admin"].includes(currentRole.value)) {
throw new Error("You do not have permission to edit member profiles");
}
if (userId === currentUser.value?.id) {
throw new Error("Use updateProfile method to edit your own profile");
}
try {
authState.value = { ...authState.value, loading: true };
const allowedFields = ["full_name", "phone", "company_role"];
const filteredUpdates = Object.keys(updates).filter((key) => allowedFields.includes(key)).reduce((obj, key) => {
obj[key] = updates[key];
return obj;
}, {});
if (Object.keys(filteredUpdates).length === 0) {
throw new Error("No valid fields to update");
}
const { error } = await getClient().from("profiles").update(filteredUpdates).eq("id", userId);
if (error) {
throw { code: "UPDATE_MEMBER_PROFILE_FAILED", message: error.message };
}
const updatedMembers = authState.value.teamMembers.map((member) => {
if (member.user_id === userId) {
return {
...member,
profile: { ...member.profile, ...filteredUpdates }
};
}
return member;
});
authState.value = {
...authState.value,
teamMembers: updatedMembers
};
} catch (error) {
console.error("Update team member profile failed:", error);
throw error;
} finally {
authState.value = { ...authState.value, loading: false };
}
},
// Utility methods
getAvatarFallback: (overrides) => {
const fullName = overrides?.fullName !== void 0 ? overrides.fullName : currentUser.value?.user_metadata?.name;
const email = overrides?.email !== void 0 ? overrides.email : currentUser.value?.email;
if (fullName && fullName.trim()) {
return fullName.trim().split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2);
}
if (email) {
return email[0].toUpperCase();
}
return "U";
},
// Unified impersonation methods (no delegation)
startImpersonation: async (targetUserId, reason) => {
try {
updateAuthState({ loading: true });
const originalUser2 = currentUser.value;
if (!originalUser2) {
throw new Error("No authenticated user to start impersonation from");
}
const { data: { session } } = await getClient().auth.getSession();
if (!session) {
throw new Error("No active session");
}
const response = await $fetch("/api/impersonate", {
method: "POST",
headers: {
Authorization: `Bearer ${session.access_token}`
},
body: { targetUserId, reason }
});
if (!response.success) {
throw new Error(response.message || "Failed to start impersonation");
}
updateAuthState({
// Keep current user for now, auth listener will update it
originalUser: originalUser2,
impersonating: true,
impersonatedUser: response.impersonation.target_user,
impersonationExpiresAt: new Date(response.impersonation.expires_at),
impersonationSessionId: response.impersonation.session_id,
loading: false
});
const toast = useToast();
toast.add({
title: "Impersonation Started",
description: `Now impersonating ${response.impersonation.target_user.full_name || response.impersonation.target_user.email}`,
color: "blue",
icon: "i-lucide-user-check"
});
authState.value = { ...authState.value, justStartedImpersonation: true };
await getClient().auth.setSession({
access_token: response.session.access_token,
refresh_token: response.session.refresh_token
});
} catch (error) {
console.error("Start impersonation failed:", error);
updateAuthState({ loading: false });
const toast = useToast();
toast.add({
title: "Impersonation Failed",
description: error.data?.message || error.message || "Failed to start impersonation",
color: "red",
icon: "i-lucide-alert-circle"
});
throw error;
}
},
stopImpersonation: async () => {
try {
updateAuthState({ loading: true });
if (!isImpersonating.value || !authState.value.impersonationSessionId) {
throw new Error("No active impersonation session");
}
const { data: { session } } = await getClient().auth.getSession();
if (!session) {
updateAuthState({
impersonating: false,
impersonatedUser: null,
impersonationExpiresAt: null,
impersonationSessionId: null,
originalUser: null,
loading: false
});
const toast2 = useToast();
toast2.add({
title: "Impersonation Cleared",
description: "Stale impersonation state has been cleared",
color: "green",
icon: "i-lucide-user-x"
});
return;
}
const response = await $fetch("/api/stop-impersonation", {
method: "POST",
headers: {
Authorization: `Bearer ${session.access_token}`
},
body: {
sessionId: authState.value.impersonationSessionId
}
});
updateAuthState({
stoppingImpersonation: true
});
updateAuthState({
impersonating: false,
impersonatedUser: null,
impersonationExpiresAt: null,
impersonationSessionId: null,
originalUser: null,
loading: false
});
if (response.session) {
try {
await getClient().auth.setSession({
access_token: response.session.access_token,
refresh_token: response.session.refresh_token
});
setTimeout(async () => {
try {
const { data: { session: session2 } } = await getClient().auth.getSession();
if (session2?.user) {
await updateCompleteAuthState(session2.user);
}
} catch (error) {
console.warn("Failed to refresh auth state after impersonation end:", error);
}
}, 100);
} catch (error) {
console.warn("Failed to restore admin session:", error);
updateAuthState({ stoppingImpersonation: false });
}
}
const toast = useToast();
toast.add({
title: "Impersonation Ended",
description: "Returned to your original session",
color: "green",
icon: "i-lucide-user-x"
});
} catch (error) {
console.error("Stop impersonation failed:", error);
updateAuthState({ loading: false });
const toast = useToast();
toast.add({
title: "Error Stopping Impersonation",
description: "Session has been cleared. Please sign in again.",
color: "red",
icon: "i-lucide-alert-circle"
});
throw error;
}
},
// Session management utilities
sessionHealth: () => sessionSync.performSessionHealthCheck(currentUser, currentTeam, currentRole, isImpersonating, impersonationExpiresAt),
triggerSessionRecovery: () => sessionSync.triggerSessionRecovery(currentUser, currentTeam, currentRole, isImpersonating, impersonationExpiresAt),
getActiveTabs: sessionSync.getActiveTabs,
isTabPrimary: sessionSync.isPrimaryTab,
// Testing utilities
$initializationPromise: Promise.resolve(),
// Force refresh auth state
refreshAuthState: async () => {
if (currentUser.value) {
await updateCompleteAuthState(currentUser.value);
}
},
// Clear success flag for UI components
clearSuccessFlag: () => {
authState.value = { ...authState.value, justStartedImpersonation: false };
}
};
}