feat: auth

This commit is contained in:
2026-01-10 02:53:53 -08:00
parent f8eef78261
commit ea59c45a76
7 changed files with 121 additions and 19 deletions

View File

@@ -4,7 +4,9 @@
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "bun scripts/watch-openapi.ts & vite",
"dev:vite": "vite",
"dev:watch": "bun scripts/watch-openapi.ts",
"build": "tsc && vite build",
"preview": "vite preview",
"test.e2e": "cypress run",

29
scripts/watch-openapi.ts Normal file
View File

@@ -0,0 +1,29 @@
import { watch } from "node:fs";
import { spawn } from "bun";
const file = "openapi.json";
console.log(`Watching ${file} for changes...`);
// Run postinstall once on startup with --force to ensure client is up to date
await spawn(["bun", "scripts/postinstall.ts", "--force"], {
stdio: ["inherit", "inherit", "inherit"],
}).exited;
let debounceTimer: Timer | null = null;
watch(file, async (eventType) => {
if (eventType !== "change") return;
// Debounce rapid changes
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
console.log(`\n${file} changed, regenerating client...`);
await spawn(["bun", "scripts/postinstall.ts", "--force"], {
stdio: ["inherit", "inherit", "inherit"],
}).exited;
}, 100);
});
// Keep the process running
await new Promise(() => {});

View File

@@ -44,8 +44,9 @@ import Settings from "./pages/settings/Settings";
import Stats from "./pages/stats/Stats";
import Workout from "./pages/workout/Workout";
import { useAuth } from "./hooks/useAuth";
import { AuthProvider, useAuth } from "./hooks/useAuth";
import Login from "./pages/login/Login";
import { useEffect } from "react";
setupIonicReact();
@@ -105,14 +106,18 @@ function UnauthenticatedRouter() {
<Route exact path="/login">
<Login />
</Route>
<Redirect exact from="/" to="/login" />
<Route render={() => <Redirect to="/login" />} />
</IonRouterOutlet>
);
}
function App() {
function AppContent() {
const { isAuthenticated, isLoading } = useAuth();
useEffect(() => {
console.log("isAuthenticated", isAuthenticated);
}, [isAuthenticated]);
return (
<IonApp>
<IonReactRouter>
@@ -123,4 +128,12 @@ function App() {
);
}
function App() {
return (
<AuthProvider>
<AppContent />
</AuthProvider>
);
}
export default App;

View File

@@ -1,4 +1,11 @@
import { useCallback, useEffect, useState } from "react";
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
type ReactNode,
} from "react";
import {
clearTokens,
getAccessToken,
@@ -13,7 +20,15 @@ interface AuthState {
refreshToken: string | null;
}
export function useAuth() {
interface AuthContextValue extends AuthState {
login: (accessToken: string, refreshToken?: string) => Promise<void>;
logout: () => Promise<void>;
checkAuth: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<AuthState>({
isAuthenticated: false,
isLoading: true,
@@ -71,10 +86,24 @@ export function useAuth() {
});
}, []);
return {
...state,
login,
logout,
checkAuth,
};
return (
<AuthContext.Provider
value={{
...state,
login,
logout,
checkAuth,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}

View File

@@ -2,7 +2,7 @@ import { client } from "./api-openapi-gen/client.gen";
client.setConfig({
baseUrl: "http://localhost:8080",
credentials: "include",
// credentials: "include",
});
export * as api from "./api-openapi-gen/sdk.gen";

View File

@@ -10,8 +10,11 @@ import {
} from "@ionic/react";
import { useCallback, useState } from "react";
import "./Login.css";
import { api } from "../../lib/api";
import { useAuth } from "../../hooks/useAuth";
export default function Login() {
const { login } = useAuth();
const [isRegistering, setIsRegistering] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
@@ -20,13 +23,35 @@ export default function Login() {
const title = isRegistering ? "Register" : "Login";
const handleLogin = useCallback(() => {
console.log("login", username, password);
}, [username, password]);
const handleLogin = useCallback(async () => {
const { data, error } = await api.login({
body: {
username,
password,
},
});
if (error || !data) {
console.error("Failed to login", error);
return;
}
await login(data);
}, [username, password, login]);
const handleRegister = useCallback(() => {
console.log("register", username, password, realName, email);
}, [username, password, realName, email]);
const handleRegister = useCallback(async () => {
const { data, error } = await api.signup({
body: {
username,
password,
real_name: realName,
email,
},
});
if (error || !data) {
console.error("Failed to register", error);
return;
}
await login(data);
}, [username, password, realName, email, login]);
const handleClick = useCallback(() => {
if (isRegistering) {

View File

@@ -1,4 +1,5 @@
import {
IonButton,
IonContent,
IonHeader,
IonPage,
@@ -7,8 +8,10 @@ import {
} from "@ionic/react";
import ExploreContainer from "../../components/ExploreContainer";
import "./Settings.css";
import { useAuth } from "../../hooks/useAuth";
export default function Settings() {
const { logout } = useAuth();
return (
<IonPage>
<IonHeader>
@@ -23,6 +26,7 @@ export default function Settings() {
</IonToolbar>
</IonHeader>
<ExploreContainer name="Settings page" />
<IonButton onClick={logout}>Logout</IonButton>
</IonContent>
</IonPage>
);