mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-29 16:06:07 +00:00
account login shit
This commit is contained in:
parent
2953b8f29f
commit
6ba57d701f
71
src/backend/accounts/auth.ts
Normal file
71
src/backend/accounts/auth.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ReactNode } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { Lightbar } from "@/components/utils/Lightbar";
|
import { Lightbar } from "@/components/utils/Lightbar";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
|
import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { useBannerSize } from "@/stores/banner";
|
import { useBannerSize } from "@/stores/banner";
|
||||||
|
@ -12,7 +12,6 @@ import { useBannerSize } from "@/stores/banner";
|
||||||
import { BrandPill } from "./BrandPill";
|
import { BrandPill } from "./BrandPill";
|
||||||
|
|
||||||
export interface NavigationProps {
|
export interface NavigationProps {
|
||||||
children?: ReactNode;
|
|
||||||
bg?: boolean;
|
bg?: boolean;
|
||||||
noLightbar?: boolean;
|
noLightbar?: boolean;
|
||||||
doBackground?: boolean;
|
doBackground?: boolean;
|
||||||
|
@ -20,6 +19,7 @@ export interface NavigationProps {
|
||||||
|
|
||||||
export function Navigation(props: NavigationProps) {
|
export function Navigation(props: NavigationProps) {
|
||||||
const bannerHeight = useBannerSize();
|
const bannerHeight = useBannerSize();
|
||||||
|
const { loggedIn } = useAuth();
|
||||||
|
|
||||||
return (
|
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 className="absolute -bottom-24 h-24 w-full bg-gradient-to-b from-background-main to-transparent" />
|
||||||
</div>
|
</div>
|
||||||
<div className="pointer-events-auto px-7 py-5 relative flex flex-1 items-center space-x-3">
|
<div className="pointer-events-auto px-7 py-5 relative flex flex-1 items-center space-x-3">
|
||||||
<Link className="block" to="/">
|
<div className="flex items-center flex-1">
|
||||||
<BrandPill clickable />
|
<Link className="block" to="/">
|
||||||
</Link>
|
<BrandPill clickable />
|
||||||
<a
|
</Link>
|
||||||
href={conf().DISCORD_LINK}
|
<a
|
||||||
target="_blank"
|
href={conf().DISCORD_LINK}
|
||||||
rel="noreferrer"
|
target="_blank"
|
||||||
className="text-xl text-white"
|
rel="noreferrer"
|
||||||
>
|
className="text-xl text-white"
|
||||||
<IconPatch icon={Icons.DISCORD} clickable downsized />
|
>
|
||||||
</a>
|
<IconPatch icon={Icons.DISCORD} clickable downsized />
|
||||||
<a
|
</a>
|
||||||
href={conf().GITHUB_LINK}
|
<a
|
||||||
target="_blank"
|
href={conf().GITHUB_LINK}
|
||||||
rel="noreferrer"
|
target="_blank"
|
||||||
className="text-xl text-white"
|
rel="noreferrer"
|
||||||
>
|
className="text-xl text-white"
|
||||||
<IconPatch icon={Icons.GITHUB} clickable downsized />
|
>
|
||||||
</a>
|
<IconPatch icon={Icons.GITHUB} clickable downsized />
|
||||||
{props.children}
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>User: {JSON.stringify(loggedIn)}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
54
src/hooks/useAuth.ts
Normal file
54
src/hooks/useAuth.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ interface Config {
|
||||||
TMDB_READ_API_KEY: string;
|
TMDB_READ_API_KEY: string;
|
||||||
CORS_PROXY_URL: string;
|
CORS_PROXY_URL: string;
|
||||||
NORMAL_ROUTER: boolean;
|
NORMAL_ROUTER: boolean;
|
||||||
|
BACKEND_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeConfig {
|
export interface RuntimeConfig {
|
||||||
|
@ -16,6 +17,7 @@ export interface RuntimeConfig {
|
||||||
TMDB_READ_API_KEY: string;
|
TMDB_READ_API_KEY: string;
|
||||||
NORMAL_ROUTER: boolean;
|
NORMAL_ROUTER: boolean;
|
||||||
PROXY_URLS: string[];
|
PROXY_URLS: string[];
|
||||||
|
BACKEND_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const env: Record<keyof Config, undefined | string> = {
|
const env: Record<keyof Config, undefined | string> = {
|
||||||
|
@ -25,6 +27,7 @@ const env: Record<keyof Config, undefined | string> = {
|
||||||
DISCORD_LINK: undefined,
|
DISCORD_LINK: undefined,
|
||||||
CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL,
|
CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL,
|
||||||
NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER,
|
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)
|
// loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js)
|
||||||
|
@ -44,6 +47,7 @@ export function conf(): RuntimeConfig {
|
||||||
APP_VERSION,
|
APP_VERSION,
|
||||||
GITHUB_LINK,
|
GITHUB_LINK,
|
||||||
DISCORD_LINK,
|
DISCORD_LINK,
|
||||||
|
BACKEND_URL: getKey("BACKEND_URL"),
|
||||||
TMDB_READ_API_KEY: getKey("TMDB_READ_API_KEY"),
|
TMDB_READ_API_KEY: getKey("TMDB_READ_API_KEY"),
|
||||||
PROXY_URLS: getKey("CORS_PROXY_URL")
|
PROXY_URLS: getKey("CORS_PROXY_URL")
|
||||||
.split(",")
|
.split(",")
|
||||||
|
|
58
src/stores/auth/index.ts
Normal file
58
src/stores/auth/index.ts
Normal 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",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
Loading…
Reference in a new issue