account login shit

This commit is contained in:
mrjvs 2023-10-31 21:08:09 +01:00
parent 2953b8f29f
commit 6ba57d701f
5 changed files with 213 additions and 22 deletions

View file

@ -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,
});
}

View file

@ -1,10 +1,10 @@
import classNames from "classnames";
import { ReactNode } from "react";
import { Link } from "react-router-dom";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { Lightbar } from "@/components/utils/Lightbar";
import { useAuth } from "@/hooks/useAuth";
import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
import { conf } from "@/setup/config";
import { useBannerSize } from "@/stores/banner";
@ -12,7 +12,6 @@ import { useBannerSize } from "@/stores/banner";
import { BrandPill } from "./BrandPill";
export interface NavigationProps {
children?: ReactNode;
bg?: boolean;
noLightbar?: boolean;
doBackground?: boolean;
@ -20,6 +19,7 @@ export interface NavigationProps {
export function Navigation(props: NavigationProps) {
const bannerHeight = useBannerSize();
const { loggedIn } = useAuth();
return (
<>
@ -60,26 +60,30 @@ export function Navigation(props: NavigationProps) {
<div className="absolute -bottom-24 h-24 w-full bg-gradient-to-b from-background-main to-transparent" />
</div>
<div className="pointer-events-auto px-7 py-5 relative flex flex-1 items-center space-x-3">
<Link className="block" to="/">
<BrandPill clickable />
</Link>
<a
href={conf().DISCORD_LINK}
target="_blank"
rel="noreferrer"
className="text-xl text-white"
>
<IconPatch icon={Icons.DISCORD} clickable downsized />
</a>
<a
href={conf().GITHUB_LINK}
target="_blank"
rel="noreferrer"
className="text-xl text-white"
>
<IconPatch icon={Icons.GITHUB} clickable downsized />
</a>
{props.children}
<div className="flex items-center flex-1">
<Link className="block" to="/">
<BrandPill clickable />
</Link>
<a
href={conf().DISCORD_LINK}
target="_blank"
rel="noreferrer"
className="text-xl text-white"
>
<IconPatch icon={Icons.DISCORD} clickable downsized />
</a>
<a
href={conf().GITHUB_LINK}
target="_blank"
rel="noreferrer"
className="text-xl text-white"
>
<IconPatch icon={Icons.GITHUB} clickable downsized />
</a>
</div>
<div>
<p>User: {JSON.stringify(loggedIn)}</p>
</div>
</div>
</div>
</div>

54
src/hooks/useAuth.ts Normal file
View file

@ -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,
};
}

View file

@ -7,6 +7,7 @@ interface Config {
TMDB_READ_API_KEY: string;
CORS_PROXY_URL: string;
NORMAL_ROUTER: boolean;
BACKEND_URL: string;
}
export interface RuntimeConfig {
@ -16,6 +17,7 @@ export interface RuntimeConfig {
TMDB_READ_API_KEY: string;
NORMAL_ROUTER: boolean;
PROXY_URLS: string[];
BACKEND_URL: string;
}
const env: Record<keyof Config, undefined | string> = {
@ -25,6 +27,7 @@ const env: Record<keyof Config, undefined | string> = {
DISCORD_LINK: undefined,
CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL,
NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER,
BACKEND_URL: import.meta.env.VITE_BACKEND_URL,
};
// loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js)
@ -44,6 +47,7 @@ export function conf(): RuntimeConfig {
APP_VERSION,
GITHUB_LINK,
DISCORD_LINK,
BACKEND_URL: getKey("BACKEND_URL"),
TMDB_READ_API_KEY: getKey("TMDB_READ_API_KEY"),
PROXY_URLS: getKey("CORS_PROXY_URL")
.split(",")

58
src/stores/auth/index.ts Normal file
View file

@ -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",
}
)
);