mirror of
https://github.com/Xevion/linkpulse.git
synced 2025-12-06 09:15:32 -06:00
Fixup hacky login session system, add separate development Caddyfile for CORS cookie issue
This commit is contained in:
@@ -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}
|
||||
|
||||
|
||||
|
||||
18
frontend/Caddyfile.development
Normal file
18
frontend/Caddyfile.development
Normal 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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }),
|
||||
}));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user