5 changed files with 213 additions and 22 deletions
@ -0,0 +1,71 @@ |
|||||||
|
import { ofetch } from "ofetch"; |
||||||
|
|
||||||
|
export interface SessionResponse { |
||||||
|
id: string; |
||||||
|
userId: string; |
||||||
|
createdAt: string; |
||||||
|
accessedAt: string; |
||||||
|
device: string; |
||||||
|
userAgent: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface UserResponse { |
||||||
|
id: string; |
||||||
|
namespace: string; |
||||||
|
name: string; |
||||||
|
roles: string[]; |
||||||
|
createdAt: string; |
||||||
|
profile: { |
||||||
|
colorA: string; |
||||||
|
colorB: string; |
||||||
|
icon: string; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export interface LoginResponse { |
||||||
|
session: SessionResponse; |
||||||
|
token: string; |
||||||
|
} |
||||||
|
|
||||||
|
function getAuthHeaders(token: string): Record<string, string> { |
||||||
|
return { |
||||||
|
authorization: `Bearer ${token}`, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export async function accountLogin( |
||||||
|
url: string, |
||||||
|
id: string, |
||||||
|
deviceName: string |
||||||
|
): Promise<LoginResponse> { |
||||||
|
return ofetch<LoginResponse>("/auth/login", { |
||||||
|
method: "POST", |
||||||
|
body: { |
||||||
|
id, |
||||||
|
device: deviceName, |
||||||
|
}, |
||||||
|
baseURL: url, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export async function getUser( |
||||||
|
url: string, |
||||||
|
token: string |
||||||
|
): Promise<UserResponse> { |
||||||
|
return ofetch<UserResponse>("/user/@me", { |
||||||
|
headers: getAuthHeaders(token), |
||||||
|
baseURL: url, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export async function removeSession( |
||||||
|
url: string, |
||||||
|
token: string, |
||||||
|
sessionId: string |
||||||
|
): Promise<UserResponse> { |
||||||
|
return ofetch<UserResponse>(`/sessions/${sessionId}`, { |
||||||
|
method: "DELETE", |
||||||
|
headers: getAuthHeaders(token), |
||||||
|
baseURL: url, |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,54 @@ |
|||||||
|
import { useCallback } from "react"; |
||||||
|
|
||||||
|
import { accountLogin, getUser, removeSession } from "@/backend/accounts/auth"; |
||||||
|
import { conf } from "@/setup/config"; |
||||||
|
import { useAuthStore } from "@/stores/auth"; |
||||||
|
|
||||||
|
export function useBackendUrl() { |
||||||
|
const backendUrl = useAuthStore((s) => s.backendUrl); |
||||||
|
return backendUrl ?? conf().BACKEND_URL; |
||||||
|
} |
||||||
|
|
||||||
|
export function useAuth() { |
||||||
|
const currentAccount = useAuthStore((s) => s.account); |
||||||
|
const profile = useAuthStore((s) => s.account?.profile); |
||||||
|
const loggedIn = !!useAuthStore((s) => s.account); |
||||||
|
const setAccount = useAuthStore((s) => s.setAccount); |
||||||
|
const removeAccount = useAuthStore((s) => s.removeAccount); |
||||||
|
const backendUrl = useBackendUrl(); |
||||||
|
|
||||||
|
const login = useCallback( |
||||||
|
async (id: string, device: string) => { |
||||||
|
const account = await accountLogin(backendUrl, id, device); |
||||||
|
const user = await getUser(backendUrl, account.token); |
||||||
|
setAccount({ |
||||||
|
token: account.token, |
||||||
|
userId: user.id, |
||||||
|
sessionId: account.session.id, |
||||||
|
profile: user.profile, |
||||||
|
}); |
||||||
|
}, |
||||||
|
[setAccount, backendUrl] |
||||||
|
); |
||||||
|
|
||||||
|
const logout = useCallback(async () => { |
||||||
|
if (!currentAccount) return; |
||||||
|
try { |
||||||
|
await removeSession( |
||||||
|
backendUrl, |
||||||
|
currentAccount.token, |
||||||
|
currentAccount.sessionId |
||||||
|
); |
||||||
|
} catch { |
||||||
|
// we dont care about failing to delete session
|
||||||
|
} |
||||||
|
removeAccount(); // TODO clear local data
|
||||||
|
}, [removeAccount, backendUrl, currentAccount]); |
||||||
|
|
||||||
|
return { |
||||||
|
loggedIn, |
||||||
|
profile, |
||||||
|
login, |
||||||
|
logout, |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,58 @@ |
|||||||
|
import { create } from "zustand"; |
||||||
|
import { persist } from "zustand/middleware"; |
||||||
|
import { immer } from "zustand/middleware/immer"; |
||||||
|
|
||||||
|
interface Account { |
||||||
|
profile: { |
||||||
|
colorA: string; |
||||||
|
colorB: string; |
||||||
|
icon: string; |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
type AccountWithToken = Account & { |
||||||
|
sessionId: string; |
||||||
|
userId: string; |
||||||
|
token: string; |
||||||
|
}; |
||||||
|
|
||||||
|
interface AuthStore { |
||||||
|
account: null | AccountWithToken; |
||||||
|
backendUrl: null | string; |
||||||
|
proxySet: null | string[]; // TODO actually use these settings
|
||||||
|
removeAccount(): void; |
||||||
|
setAccount(acc: AccountWithToken): void; |
||||||
|
updateAccount(acc: Account): void; |
||||||
|
} |
||||||
|
|
||||||
|
export const useAuthStore = create( |
||||||
|
persist( |
||||||
|
immer<AuthStore>((set) => ({ |
||||||
|
account: null, |
||||||
|
backendUrl: null, |
||||||
|
proxySet: null, |
||||||
|
setAccount(acc) { |
||||||
|
set((s) => { |
||||||
|
s.account = acc; |
||||||
|
}); |
||||||
|
}, |
||||||
|
removeAccount() { |
||||||
|
set((s) => { |
||||||
|
s.account = null; |
||||||
|
}); |
||||||
|
}, |
||||||
|
updateAccount(acc) { |
||||||
|
set((s) => { |
||||||
|
if (!s.account) return; |
||||||
|
s.account = { |
||||||
|
...s.account, |
||||||
|
...acc, |
||||||
|
}; |
||||||
|
}); |
||||||
|
}, |
||||||
|
})), |
||||||
|
{ |
||||||
|
name: "__MW::auth", |
||||||
|
} |
||||||
|
) |
||||||
|
); |
Loading…
Reference in new issue