mirror of
https://github.com/Xevion/linkpulse.git
synced 2026-01-31 04:24:46 -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 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}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 { 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
Reference in New Issue
Block a user