Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions packages/web/src/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ const AuthContext = createContext<AuthContextType>(null!);
const STORAGE_KEY_TOKEN = 'stagepass_token';
const STORAGE_KEY_USER = 'stagepass_user';

interface AuthResponse {
token: string;
user: User;
}

function isAuthResponse(data: unknown): data is AuthResponse {
if (typeof data !== 'object' || data === null) return false;
const d = data as Record<string, unknown>;
return typeof d.token === 'string' && typeof d.user === 'object' && d.user !== null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The isAuthResponse type guard is a great addition for runtime safety. However, it only checks that user is a non-null object. It doesn't validate the shape of the user object itself. If the API returns a user object with missing properties (e.g., no name), it could lead to runtime errors in other components that consume this context (like Header.tsx which accesses user.name).

To make this type guard more robust, I suggest also validating the properties of the user object. Based on its usage elsewhere, the User type seems to be { id: number; name: string; email: string; }.

Suggested change
return typeof d.token === 'string' && typeof d.user === 'object' && d.user !== null;
return (
typeof d.token === 'string' &&
typeof d.user === 'object' &&
d.user !== null &&
typeof (d.user as Record<string, unknown>).id === 'number' &&
typeof (d.user as Record<string, unknown>).name === 'string' &&
typeof (d.user as Record<string, unknown>).email === 'string'
);

}

export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
Expand All @@ -42,10 +53,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
body: JSON.stringify({ email, password }),
});
if (!res.ok) throw new Error('Login failed');
const data = await res.json() as { token: string; user: User };
const data: unknown = await res.json();
if (!isAuthResponse(data)) throw new Error('Unexpected response format from server');
localStorage.setItem(STORAGE_KEY_TOKEN, data.token);
localStorage.setItem(STORAGE_KEY_USER, JSON.stringify(data.user));
setUser(data.user);
Expand All @@ -56,10 +68,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const res = await fetch('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, password })
body: JSON.stringify({ name, email, password }),
});
if (!res.ok) throw new Error('Signup failed');
const data = await res.json() as { token: string; user: User };
const data: unknown = await res.json();
if (!isAuthResponse(data)) throw new Error('Unexpected response format from server');
localStorage.setItem(STORAGE_KEY_TOKEN, data.token);
localStorage.setItem(STORAGE_KEY_USER, JSON.stringify(data.user));
setUser(data.user);
Expand Down
Loading