Skip to content

Commit

Permalink
New pages file tree (#378)
Browse files Browse the repository at this point in the history
  • Loading branch information
juancwu authored Feb 3, 2025
2 parents d8554a4 + d75030b commit bbcc51a
Show file tree
Hide file tree
Showing 28 changed files with 1,268 additions and 1,343 deletions.
40 changes: 40 additions & 0 deletions backend/internal/v1/v1_auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,25 @@ func (h *Handler) handleRegister(c echo.Context) error {
return v1_common.Fail(c, http.StatusCreated, "Registration complete but failed to sign in. Please sign in manually.", err)
}

var companyId *string = nil
company, err := q.GetCompanyByOwnerID(c.Request().Context(), newUser.ID)
if err != nil {
// just log the reason why it failed to fetch company id on login
logger.Warn(fmt.Sprintf("Error getting company on login: %s", err.Error()))
} else {
// do not return bad request on error because user might not have created a company yet
// in case the error is
companyId = &company.ID
}

// set the refresh token cookie
setRefreshTokenCookie(c, refreshToken)

logger.Info(fmt.Sprintf("New user created with email: %s", newUser.Email))

return c.JSON(http.StatusCreated, AuthResponse{
AccessToken: accessToken,
CompanyId: companyId,
User: UserResponse{
ID: newUser.ID,
Email: newUser.Email,
Expand All @@ -174,6 +186,8 @@ func (h *Handler) handleRegister(c echo.Context) error {
* 4. Returns access token and user info
*/
func (h *Handler) handleLogin(c echo.Context) error {
logger := middleware.GetLogger(c)

var req AuthRequest
if err := c.Bind(&req); err != nil {
return v1_common.Fail(c, http.StatusBadRequest, "Invalid request format", err)
Expand All @@ -199,10 +213,22 @@ func (h *Handler) handleLogin(c echo.Context) error {
return v1_common.Fail(c, http.StatusInternalServerError, "Failed to generate tokens", err)
}

var companyId *string = nil
company, err := queries.GetCompanyByOwnerID(c.Request().Context(), user.ID)
if err != nil {
// just log the reason why it failed to fetch company id on login
logger.Warn(fmt.Sprintf("Error getting company on login: %s", err.Error()))
} else {
// do not return bad request on error because user might not have created a company yet
// in case the error is
companyId = &company.ID
}

setRefreshTokenCookie(c, refreshToken)

return c.JSON(http.StatusOK, AuthResponse{
AccessToken: accessToken,
CompanyId: companyId,
User: UserResponse{
ID: user.ID,
FirstName: user.FirstName,
Expand Down Expand Up @@ -346,6 +372,8 @@ it is essentially a passwordless login for the user given that the refresh token
in the cookie is valid.
*/
func (h *Handler) handleVerifyCookie(c echo.Context) error {
logger := middleware.GetLogger(c)

cookie, err := c.Cookie(COOKIE_REFRESH_TOKEN)
if err != nil {
return v1_common.Fail(c, http.StatusUnauthorized, "Missing refresh token cookie in request", err)
Expand Down Expand Up @@ -386,8 +414,20 @@ func (h *Handler) handleVerifyCookie(c echo.Context) error {
setRefreshTokenCookie(c, refreshToken)
}

var companyId *string = nil
company, err := h.server.GetQueries().GetCompanyByOwnerID(c.Request().Context(), user.ID)
if err != nil {
// just log the reason why it failed to fetch company id on login
logger.Warn(fmt.Sprintf("Error getting company on login: %s", err.Error()))
} else {
// do not return bad request on error because user might not have created a company yet
// in case the error is
companyId = &company.ID
}

return c.JSON(http.StatusOK, AuthResponse{
AccessToken: accessToken,
CompanyId: companyId,
User: UserResponse{
ID: user.ID,
FirstName: user.FirstName,
Expand Down
3 changes: 2 additions & 1 deletion backend/internal/v1/v1_auth/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ type AuthRequest struct {

type AuthResponse struct {
AccessToken string `json:"access_token"` // jwt access token
User UserResponse `json:"user"` // user info
CompanyId *string `json:"company_id"`
User UserResponse `json:"user"` // user info
}

type UserResponse struct {
Expand Down
26 changes: 15 additions & 11 deletions frontend/src/components/VerifyEmail/VerifyEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { Button } from '@/components';
import { useAuth } from '@/contexts';
import { checkEmailVerifiedStatus } from '@/services/auth';
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';

interface VerifyEmailProps {
email: string;
Expand All @@ -18,26 +18,30 @@ export function VerifyEmail({
isResending,
}: VerifyEmailProps) {
const { user, accessToken } = useAuth();
const intervalRef = useRef<number | null>(null);

useEffect(() => {
if (user && user.emailVerified) return;

let id: number;
if (accessToken) {
id = window.setInterval(async () => {
const verified = await checkEmailVerifiedStatus(accessToken);
if (verified) {
onVerified();
}
}, 3000);
if (intervalRef.current === null) {
intervalRef.current = window.setInterval(async () => {
const verified =
await checkEmailVerifiedStatus(accessToken);
if (verified) {
onVerified();
}
}, 3000);
}
}

return () => {
if (id !== undefined) {
window.clearInterval(id);
if (intervalRef.current !== null) {
window.clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, []);
}, [accessToken]);

return (
<div className="w-full max-w-md mx-auto p-6 bg-white rounded-lg shadow-md text-center">
Expand Down
64 changes: 36 additions & 28 deletions frontend/src/contexts/AuthContext/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import React, {
createContext,
useContext,
useState,
useEffect,
useRef,
} from 'react';
import { refreshAccessToken, signout } from '@/services/auth';
import type { User } from '@/types';
import { snakeToCamel } from '@/utils/object';
Expand Down Expand Up @@ -26,15 +32,38 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const [companyId, setCompanyId] = useState<string | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const intervalRef = useRef<number | null>(null);

useEffect(() => {
const verifyAuth = async () => {
try {
const response = await refreshAccessToken();
if (response) {
console.log(response);
setUser(snakeToCamel(response.user));
setAccessToken(response.access_token);
setAccessToken(response.accessToken);
setCompanyId(response.companyId);

if (intervalRef.current === null) {
const REFRESH_INTERVAL = 1000 * 60 * 4; // 4 minutes (just under the 5-minute backend token expiry)
const refreshToken = async () => {
try {
const response = await refreshAccessToken();
if (response) {
setAccessToken(response.accessToken);
}
} catch (error) {
console.error(
'Failed to refresh token:',
error
);
clearAuth();
}
};
intervalRef.current = window.setInterval(
refreshToken,
REFRESH_INTERVAL
);
}
}
} catch (error) {
console.error('Initial auth verification failed:', error);
Expand All @@ -44,35 +73,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
};

verifyAuth();
}, []);

useEffect(() => {
if (!accessToken) return;

const REFRESH_INTERVAL = 1000 * 60 * 4; // 4 minutes (just under the 5-minute backend token expiry)
let refreshTimeout: NodeJS.Timeout | null = null;

const refreshToken = async () => {
try {
const response = await refreshAccessToken();
if (response) {
setUser(response.user);
setAccessToken(response.access_token);
}
} catch (error) {
console.error('Failed to refresh token:', error);
clearAuth();
}
};

refreshTimeout = setInterval(refreshToken, REFRESH_INTERVAL);

return () => {
if (refreshTimeout) {
clearInterval(refreshTimeout);
if (intervalRef.current !== null) {
window.clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [accessToken]);
}, []);

const setAuth = (
newUser: User | null,
Expand Down
18 changes: 8 additions & 10 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

// Create router after auth is ready
const router = createRouter({
routeTree,
context: {
auth: undefined,
},
});

function Router() {
const auth = useAuth();

const router = createRouter({
routeTree,
context: {
auth,
},
});

return <RouterProvider router={router} />;
return <RouterProvider router={router} context={{ auth }} />;
}

// Render the app
Expand Down
Loading

0 comments on commit bbcc51a

Please sign in to comment.