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

View File

@@ -5,7 +5,7 @@ import structlog
from fastapi import APIRouter, Depends, Response, status
from linkpulse.dependencies import RateLimiter, SessionDependency
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.hashers.argon2 import Argon2Hasher
from pydantic import BaseModel, EmailStr, Field
@@ -109,7 +109,8 @@ async def login(body: LoginBody, response: Response):
)
# 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}

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
}

View File

@@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { login } from "@/lib/auth";
import { useUserStore } from "@/lib/state";
import { cn } from "@/lib/utils";
import { Link } from "@tanstack/react-router";
import { HTMLAttributes, SyntheticEvent, useState } from "react";
@@ -38,7 +39,7 @@ export function RegisterForm({ className, ...props }: UserAuthFormProps) {
disabled={isLoading}
/>
</div>
<Button disabled={isLoading}>
<Button disabled={isLoading || true}>
{isLoading && (
<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) {
const [isLoading, setIsLoading] = useState<boolean>(false);
const { setUser } = useUserStore();
async function onSubmit(event: SyntheticEvent) {
event.preventDefault();
@@ -61,7 +63,11 @@ export function LoginForm({ className, ...props }: UserAuthFormProps) {
setIsLoading(true);
const result = await login(email, password);
console.log({ result });
if (result.isOk) {
setUser(result.value);
} else {
alert("Login failed");
}
setIsLoading(false);
}

View File

@@ -1,5 +1,5 @@
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.
// 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.
// 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}`
: "";
console.log({ env: import.meta.env, target: TARGET });
type ErrorResponse = {
detail: string;
};
@@ -21,6 +20,14 @@ type SessionResponse = {
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<
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 = {
email: string;
expiry: string;

View File

@@ -1,6 +1,7 @@
import { create } from "zustand";
type UserState = {
initialized: boolean;
user: {
// TODO: This will eventually carry more user information (name, avatar, etc.)
email: string;
@@ -13,7 +14,8 @@ type UserActions = {
};
export const useUserStore = create<UserState & UserActions>((set) => ({
initialized: false,
user: null,
setUser: (user) => set({ user }),
setUser: (user) => set({ user, initialized: true }),
logout: () => set({ user: null }),
}));

View File

@@ -10,8 +10,6 @@ import { routeTree } from "./routeTree.gen";
// Create a new router instance
const router = createRouter({ routeTree });
getSession();
// Register the router instance for type safety
declare module "@tanstack/react-router" {
interface Register {

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")({
component: RouteComponent,
beforeLoad: async ({ location }) => {
const authenticated = await isAuthenticated();
if (!authenticated) {
return redirect({
to: "/login",
search: { redirect: location.href },
});
}
},
});
function RouteComponent() {
return "Hello /dashboard!";
const { user } = useUserStore();
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user?.email || "null"}</p>
</div>
);
}

View File

@@ -1,14 +1,14 @@
import { useUserStore } from "@/lib/state";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { LoginForm } from "@/components/auth/form";
import AuthenticationPage from "@/components/pages/authentication";
import { isAuthenticated } from "@/lib/auth";
export const Route = createFileRoute("/login")({
beforeLoad: async ({ location }) => {
const isLoggedIn = useUserStore.getState().user !== null;
const authenticated = await isAuthenticated();
if (isLoggedIn) {
if (authenticated) {
return redirect({
to: "/dashboard",
search: { redirect: location.href },