Fixup hacky login session system, add separate development Caddyfile for CORS cookie issue

This commit is contained in:
2024-11-10 23:40:00 -06:00
parent 9990bcab02
commit f14285b252
8 changed files with 76 additions and 15 deletions
+3 -2
View File
@@ -5,7 +5,7 @@ import structlog
from fastapi import APIRouter, Depends, Response, status from fastapi import APIRouter, Depends, Response, status
from linkpulse.dependencies import RateLimiter, SessionDependency from linkpulse.dependencies import RateLimiter, SessionDependency
from linkpulse.models import Session, User from linkpulse.models import Session, User
from linkpulse.utilities import utc_now from linkpulse.utilities import utc_now, is_development
from pwdlib import PasswordHash from pwdlib import PasswordHash
from pwdlib.hashers.argon2 import Argon2Hasher from pwdlib.hashers.argon2 import Argon2Hasher
from pydantic import BaseModel, EmailStr, Field from pydantic import BaseModel, EmailStr, Field
@@ -109,7 +109,8 @@ async def login(body: LoginBody, response: Response):
) )
# Set Cookie of session token # Set Cookie of session token
response.set_cookie("session", token, samesite="strict", max_age=int(session_duration.total_seconds())) max_age = int(session_duration.total_seconds())
response.set_cookie("session", token, max_age=max_age, secure=not is_development, httponly=True)
return {"email": user.email, "expiry": session.expiry} return {"email": user.email, "expiry": session.expiry}
+18
View File
@@ -0,0 +1,18 @@
{
admin off # theres no need for the admin api in railway's environment
auto_https off # railway handles https for us, this would cause issues if left enabled
}
http://localhost:8080 {
respond /health 200
encode {
zstd fastest
gzip 3
}
handle /api/* {
reverse_proxy localhost:8000
}
reverse_proxy localhost:5173
}
+8 -2
View File
@@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { login } from "@/lib/auth"; import { login } from "@/lib/auth";
import { useUserStore } from "@/lib/state";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { HTMLAttributes, SyntheticEvent, useState } from "react"; import { HTMLAttributes, SyntheticEvent, useState } from "react";
@@ -38,7 +39,7 @@ export function RegisterForm({ className, ...props }: UserAuthFormProps) {
disabled={isLoading} disabled={isLoading}
/> />
</div> </div>
<Button disabled={isLoading}> <Button disabled={isLoading || true}>
{isLoading && ( {isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" /> <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)} )}
@@ -52,6 +53,7 @@ export function RegisterForm({ className, ...props }: UserAuthFormProps) {
export function LoginForm({ className, ...props }: UserAuthFormProps) { export function LoginForm({ className, ...props }: UserAuthFormProps) {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const { setUser } = useUserStore();
async function onSubmit(event: SyntheticEvent) { async function onSubmit(event: SyntheticEvent) {
event.preventDefault(); event.preventDefault();
@@ -61,7 +63,11 @@ export function LoginForm({ className, ...props }: UserAuthFormProps) {
setIsLoading(true); setIsLoading(true);
const result = await login(email, password); const result = await login(email, password);
console.log({ result }); if (result.isOk) {
setUser(result.value);
} else {
alert("Login failed");
}
setIsLoading(false); setIsLoading(false);
} }
+20 -3
View File
@@ -1,5 +1,5 @@
import Result, { ok, err } from "true-myth/result"; import Result, { ok, err } from "true-myth/result";
import { useUserStore } from "./state"; import { useUserStore } from "@/lib/state";
// Authentication on the frontend is managed by Zustand's state. // Authentication on the frontend is managed by Zustand's state.
// Upon application load, a single request is made to acquire session state. // Upon application load, a single request is made to acquire session state.
@@ -8,11 +8,10 @@ import { useUserStore } from "./state";
// If any protected API call suddenly fails with a 401 status code, the user store is reset, a logout message is displayed, and the user is redirected to the login page. // If any protected API call suddenly fails with a 401 status code, the user store is reset, a logout message is displayed, and the user is redirected to the login page.
// All redirects to the login page will carry a masked URL parameter that can be used to redirect the user back to the page they were on after logging in. // All redirects to the login page will carry a masked URL parameter that can be used to redirect the user back to the page they were on after logging in.
const TARGET = import.meta.env.DEV const TARGET = !import.meta.env.DEV
? `http://${import.meta.env.VITE_BACKEND_TARGET}` ? `http://${import.meta.env.VITE_BACKEND_TARGET}`
: ""; : "";
console.log({ env: import.meta.env, target: TARGET });
type ErrorResponse = { type ErrorResponse = {
detail: string; detail: string;
}; };
@@ -21,6 +20,14 @@ type SessionResponse = {
user: {}; user: {};
}; };
/**
* Retrieves the current session from the server.
*
* An Ok result will contain the session data, implying that the session is valid and the user is authenticated.
* An Err result will contain an error response, implying that the session is invalid or non-existent, and the user is not authenticated.
*
* @returns {Promise<Result<SessionResponse, ErrorResponse>>} A promise that resolves to a Result object containing either the session data or an error response.
*/
export const getSession = async (): Promise< export const getSession = async (): Promise<
Result<SessionResponse, ErrorResponse> Result<SessionResponse, ErrorResponse>
> => { > => {
@@ -36,6 +43,16 @@ export const getSession = async (): Promise<
} }
}; };
export const isAuthenticated = async (): Promise<boolean> => {
let state = useUserStore.getState();
if (state.initialized) return state.user !== null;
else {
const result = await getSession();
return result.isOk;
}
};
type LoginResponse = { type LoginResponse = {
email: string; email: string;
expiry: string; expiry: string;
+3 -1
View File
@@ -1,6 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
type UserState = { type UserState = {
initialized: boolean;
user: { user: {
// TODO: This will eventually carry more user information (name, avatar, etc.) // TODO: This will eventually carry more user information (name, avatar, etc.)
email: string; email: string;
@@ -13,7 +14,8 @@ type UserActions = {
}; };
export const useUserStore = create<UserState & UserActions>((set) => ({ export const useUserStore = create<UserState & UserActions>((set) => ({
initialized: false,
user: null, user: null,
setUser: (user) => set({ user }), setUser: (user) => set({ user, initialized: true }),
logout: () => set({ user: null }), logout: () => set({ user: null }),
})); }));
-2
View File
@@ -10,8 +10,6 @@ import { routeTree } from "./routeTree.gen";
// Create a new router instance // Create a new router instance
const router = createRouter({ routeTree }); const router = createRouter({ routeTree });
getSession();
// Register the router instance for type safety // Register the router instance for type safety
declare module "@tanstack/react-router" { declare module "@tanstack/react-router" {
interface Register { interface Register {
+21 -2
View File
@@ -1,9 +1,28 @@
import { createFileRoute } from "@tanstack/react-router"; import { useUserStore } from "@/lib/state";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { isAuthenticated } from "@/lib/auth";
export const Route = createFileRoute("/dashboard")({ export const Route = createFileRoute("/dashboard")({
component: RouteComponent, component: RouteComponent,
beforeLoad: async ({ location }) => {
const authenticated = await isAuthenticated();
if (!authenticated) {
return redirect({
to: "/login",
search: { redirect: location.href },
});
}
},
}); });
function RouteComponent() { function RouteComponent() {
return "Hello /dashboard!"; const { user } = useUserStore();
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user?.email || "null"}</p>
</div>
);
} }
+3 -3
View File
@@ -1,14 +1,14 @@
import { useUserStore } from "@/lib/state";
import { createFileRoute, redirect } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { LoginForm } from "@/components/auth/form"; import { LoginForm } from "@/components/auth/form";
import AuthenticationPage from "@/components/pages/authentication"; import AuthenticationPage from "@/components/pages/authentication";
import { isAuthenticated } from "@/lib/auth";
export const Route = createFileRoute("/login")({ export const Route = createFileRoute("/login")({
beforeLoad: async ({ location }) => { beforeLoad: async ({ location }) => {
const isLoggedIn = useUserStore.getState().user !== null; const authenticated = await isAuthenticated();
if (isLoggedIn) { if (authenticated) {
return redirect({ return redirect({
to: "/dashboard", to: "/dashboard",
search: { redirect: location.href }, search: { redirect: location.href },