Changeset 3ebe47c


Ignore:
Timestamp:
02/09/26 21:36:40 (5 months ago)
Author:
Andrej <asumanovski@…>
Branches:
master
Children:
8615885
Parents:
140d098
Message:

Made authentication on frontend and backend

Files:
4 added
1 deleted
20 edited

Legend:

Unmodified
Added
Removed
  • backend/pom.xml

    r140d098 r3ebe47c  
    9999            <scope>test</scope>
    100100        </dependency>
     101
     102        <dependency>
     103            <groupId>com.h2database</groupId>
     104            <artifactId>h2</artifactId>
     105            <scope>test</scope>
     106        </dependency>
    101107    </dependencies>
    102108
  • backend/src/main/java/com/trekr/backend/BackendApplication.java

    r140d098 r3ebe47c  
    1212        // This ensures system properties are available for ${} placeholder resolution
    1313        Dotenv dotenv = loadEnvironmentVariables();
    14        
     14
    1515        // Create SpringApplication instance
    1616        SpringApplication app = new SpringApplication(BackendApplication.class);
    17        
     17
    1818        // Set system properties and default properties from .env file
    1919        if (dotenv != null) {
    2020            java.util.Map<String, Object> defaultProps = new java.util.HashMap<>();
    21            
     21
    2222            dotenv.entries().forEach(entry -> {
    2323                String key = entry.getKey();
     
    3030                }
    3131            });
    32            
     32
    3333            // Set all default properties at once
    3434            if (!defaultProps.isEmpty()) {
     
    3636            }
    3737        }
    38        
     38
    3939        app.run(args);
    4040    }
    41    
     41
    4242    private static Dotenv loadEnvironmentVariables() {
    4343        Dotenv dotenv = null;
    44         try {
    45             // Try current directory (backend/)
    46             dotenv = Dotenv.configure()
    47                     .directory("./")
    48                     .ignoreIfMissing()
    49                     .load();
    50         } catch (Exception e) {
    51             // Try parent directory if current doesn't work
     44
     45        // Prefer a standard ".env" file, but also support the existing "env" file.
     46        // We search multiple locations so it works whether the app is launched from:
     47        // - backend/ (common in IDE)
     48        // - repo root (common when running a built JAR)
     49        String[][] candidates = new String[][] {
     50                { "./", ".env" },
     51                { "./", "env" },
     52                { "./backend", ".env" },
     53                { "./backend", "env" },
     54                { "../", ".env" },
     55                { "../", "env" },
     56                { "../backend", ".env" },
     57                { "../backend", "env" },
     58        };
     59
     60        for (String[] candidate : candidates) {
    5261            try {
    53                 dotenv = Dotenv.configure()
    54                         .directory("../")
     62                Dotenv loaded = Dotenv.configure()
     63                        .directory(candidate[0])
     64                        .filename(candidate[1])
    5565                        .ignoreIfMissing()
    5666                        .load();
    57             } catch (Exception e2) {
    58                 System.err.println("WARNING: Could not load .env file. Make sure .env exists in backend/ directory.");
    59                 System.err.println("Error: " + e2.getMessage());
    60                 return null;
     67                if (loaded != null && !loaded.entries().isEmpty()) {
     68                    dotenv = loaded;
     69                    break;
     70                }
     71            } catch (Exception ignored) {
     72                // keep searching
    6173            }
    6274        }
    63        
    64         // Verify critical properties are loaded
    65         if (dotenv != null) {
    66             String url = dotenv.get("SPRING_DATASOURCE_URL");
    67             String username = dotenv.get("SPRING_DATASOURCE_USERNAME");
    68             String password = dotenv.get("SPRING_DATASOURCE_PASSWORD");
    69            
    70             if (url != null && !url.trim().isEmpty()) {
    71                 System.out.println("✓ Database URL loaded from .env");
    72             } else {
    73                 System.err.println("✗ ERROR: SPRING_DATASOURCE_URL not found in .env file!");
    74             }
    75             if (username != null && !username.trim().isEmpty()) {
    76                 System.out.println("✓ Database username loaded from .env");
    77             } else {
    78                 System.err.println("✗ ERROR: SPRING_DATASOURCE_USERNAME not found in .env file!");
    79             }
    80             if (password != null && !password.trim().isEmpty()) {
    81                 System.out.println("✓ Database password loaded from .env");
    82             } else {
    83                 System.err.println("✗ ERROR: SPRING_DATASOURCE_PASSWORD not found in .env file!");
    84             }
    85         }
    86        
     75
    8776        return dotenv;
    8877    }
  • backend/src/main/java/com/trekr/backend/config/SecurityConfig.java

    r140d098 r3ebe47c  
    33import org.springframework.context.annotation.Bean;
    44import org.springframework.context.annotation.Configuration;
     5import org.springframework.security.config.Customizer;
    56import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    67import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
     
    89import org.springframework.security.crypto.password.PasswordEncoder;
    910import org.springframework.security.web.SecurityFilterChain;
     11import org.springframework.web.cors.CorsConfiguration;
     12import org.springframework.web.cors.CorsConfigurationSource;
     13import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
     14
     15import java.util.List;
    1016
    1117@Configuration
     
    1723        http
    1824                .csrf(AbstractHttpConfigurer::disable)
     25                .cors(Customizer.withDefaults())
    1926                .authorizeHttpRequests(auth -> auth
    2027                        .requestMatchers("/api/auth/**").permitAll()
    21                         .anyRequest().authenticated()
    22                 );
     28                        .anyRequest().authenticated());
    2329        return http.build();
     30    }
     31
     32    @Bean
     33    public CorsConfigurationSource corsConfigurationSource() {
     34        CorsConfiguration config = new CorsConfiguration();
     35        config.setAllowedOrigins(List.of(
     36                "http://localhost:5173",
     37                "http://127.0.0.1:5173"));
     38        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
     39        config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
     40
     41        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
     42        source.registerCorsConfiguration("/**", config);
     43        return source;
    2444    }
    2545
  • backend/src/main/java/com/trekr/backend/controller/AuthController.java

    r140d098 r3ebe47c  
    66import com.trekr.backend.service.AuthService;
    77import jakarta.validation.Valid;
    8 import lombok.RequiredArgsConstructor;
    98import org.springframework.http.HttpStatus;
    109import org.springframework.http.ResponseEntity;
     
    1312@RestController
    1413@RequestMapping("/api/auth")
    15 @RequiredArgsConstructor
    1614public class AuthController {
    17    
     15
    1816    private final AuthService authService;
    19    
     17
     18    public AuthController(AuthService authService) {
     19        this.authService = authService;
     20    }
     21
    2022    @PostMapping("/register")
    2123    public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
     
    2325        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    2426    }
    25    
     27
    2628    @PostMapping("/login")
    2729    public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
  • backend/src/main/java/com/trekr/backend/dto/auth/AuthResponse.java

    r140d098 r3ebe47c  
    11package com.trekr.backend.dto.auth;
    22
    3 import lombok.AllArgsConstructor;
    4 import lombok.Data;
    5 import lombok.NoArgsConstructor;
    6 
    7 @Data
    8 @NoArgsConstructor
    9 @AllArgsConstructor
    103public class AuthResponse {
    114    private String token;
     
    147    private String username;
    158    private String email;
     9
     10    public AuthResponse() {
     11    }
     12
     13    public AuthResponse(String token, String type, Long userId, String username, String email) {
     14        this.token = token;
     15        this.type = type;
     16        this.userId = userId;
     17        this.username = username;
     18        this.email = email;
     19    }
     20
     21    public String getToken() {
     22        return token;
     23    }
     24
     25    public void setToken(String token) {
     26        this.token = token;
     27    }
     28
     29    public String getType() {
     30        return type;
     31    }
     32
     33    public void setType(String type) {
     34        this.type = type;
     35    }
     36
     37    public Long getUserId() {
     38        return userId;
     39    }
     40
     41    public void setUserId(Long userId) {
     42        this.userId = userId;
     43    }
     44
     45    public String getUsername() {
     46        return username;
     47    }
     48
     49    public void setUsername(String username) {
     50        this.username = username;
     51    }
     52
     53    public String getEmail() {
     54        return email;
     55    }
     56
     57    public void setEmail(String email) {
     58        this.email = email;
     59    }
    1660}
  • backend/src/main/java/com/trekr/backend/dto/auth/LoginRequest.java

    r140d098 r3ebe47c  
    22
    33import jakarta.validation.constraints.NotBlank;
    4 import lombok.Data;
    54
    6 @Data
    75public class LoginRequest {
    8    
     6
    97    @NotBlank(message = "Username or email is required")
    108    private String usernameOrEmail;
    11    
     9
    1210    @NotBlank(message = "Password is required")
    1311    private String password;
     12
     13    public LoginRequest() {
     14    }
     15
     16    public String getUsernameOrEmail() {
     17        return usernameOrEmail;
     18    }
     19
     20    public void setUsernameOrEmail(String usernameOrEmail) {
     21        this.usernameOrEmail = usernameOrEmail;
     22    }
     23
     24    public String getPassword() {
     25        return password;
     26    }
     27
     28    public void setPassword(String password) {
     29        this.password = password;
     30    }
    1431}
  • backend/src/main/java/com/trekr/backend/dto/auth/RegisterRequest.java

    r140d098 r3ebe47c  
    44import jakarta.validation.constraints.NotBlank;
    55import jakarta.validation.constraints.Size;
    6 import lombok.Data;
    76
    8 @Data
    97public class RegisterRequest {
    10    
     8
    119    @NotBlank(message = "Email is required")
    1210    @Email(message = "Email must be valid")
    1311    private String email;
    14    
     12
    1513    @NotBlank(message = "Username is required")
    1614    @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
    1715    private String username;
    18    
     16
    1917    @NotBlank(message = "Password is required")
    2018    @Size(min = 6, message = "Password must be at least 6 characters")
    2119    private String password;
     20
     21    public RegisterRequest() {
     22    }
     23
     24    public String getEmail() {
     25        return email;
     26    }
     27
     28    public void setEmail(String email) {
     29        this.email = email;
     30    }
     31
     32    public String getUsername() {
     33        return username;
     34    }
     35
     36    public void setUsername(String username) {
     37        this.username = username;
     38    }
     39
     40    public String getPassword() {
     41        return password;
     42    }
     43
     44    public void setPassword(String password) {
     45        this.password = password;
     46    }
    2247}
  • backend/src/main/java/com/trekr/backend/entity/User.java

    r140d098 r3ebe47c  
    22
    33import jakarta.persistence.*;
    4 import lombok.AllArgsConstructor;
    5 import lombok.Getter;
    6 import lombok.NoArgsConstructor;
    7 import lombok.Setter;
    84
    95@Entity
    106@Table(name = "USERS", schema = "trekr")
    11 @Getter
    12 @Setter
    13 @NoArgsConstructor
    14 @AllArgsConstructor
    157public class User {
    16    
     8
    179    @Id
    18     @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
    19     @SequenceGenerator(name = "user_seq", sequenceName = "user_id_seq", allocationSize = 1)
     10    @GeneratedValue(strategy = GenerationType.IDENTITY)
    2011    @Column(name = "user_id")
    2112    private Long userId;
    22    
     13
    2314    @Column(name = "email", nullable = false, unique = true)
    2415    private String email;
    25    
     16
    2617    @Column(name = "username", nullable = false, unique = true)
    2718    private String username;
    28    
     19
    2920    @Column(name = "password", nullable = false)
    3021    private String password;
     22
     23    public User() {
     24    }
     25
     26    public User(Long userId, String email, String username, String password) {
     27        this.userId = userId;
     28        this.email = email;
     29        this.username = username;
     30        this.password = password;
     31    }
     32
     33    public Long getUserId() {
     34        return userId;
     35    }
     36
     37    public void setUserId(Long userId) {
     38        this.userId = userId;
     39    }
     40
     41    public String getEmail() {
     42        return email;
     43    }
     44
     45    public void setEmail(String email) {
     46        this.email = email;
     47    }
     48
     49    public String getUsername() {
     50        return username;
     51    }
     52
     53    public void setUsername(String username) {
     54        this.username = username;
     55    }
     56
     57    public String getPassword() {
     58        return password;
     59    }
     60
     61    public void setPassword(String password) {
     62        this.password = password;
     63    }
    3164}
  • backend/src/main/java/com/trekr/backend/resources/application.properties

    r140d098 r3ebe47c  
    1 # application.properties
     1### NOTE
     2### This file is intentionally NOT used by Spring Boot.
     3### The active config is in: src/main/resources/application.properties
    24
    3 # General app config
    4 spring.application.name=trekr
    5 
    6 #spring.datasource.hikari.data-source-properties.useServerPrepStmts=false
    7 #spring.datasource.hikari.data-source-properties.prepareThreshold=0
    8 
    9 logging.level.org.springframework.security=DEBUG
    10 
    11 # JPA & Hibernate
    12 spring.jpa.hibernate.ddl-auto=update
    13 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
    14 spring.jpa.show-sql=false
    15 spring.jpa.properties.hibernate.format_sql=true
    16 spring.jpa.properties.hibernate.default_schema=trekr
    17 
    18 #logging.level.org.hibernate.SQL=DEBUG
    19 #logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
    20 
    21 
    22 # HikariCP settings
    23 spring.datasource.hikari.pool-name=trekrHikariPool
    24 
    25 # Maximum connections in the pool
    26 # Rule of thumb: (CPU cores × 2) + effective_spindle_count
    27 # For most apps: 10-20 is good
    28 spring.datasource.hikari.maximum-pool-size=20
    29 
    30 # Minimum idle connections always ready
    31 # Keep at least 5 ready for quick response
    32 spring.datasource.hikari.minimum-idle=5
    33 
    34 # How long to wait for a connection (before blocking the request) - 30sec
    35 spring.datasource.hikari.connection-timeout=30000
    36 
    37 # How long a connection can sit idle (before being closed) - 10min
    38 spring.datasource.hikari.idle-timeout=600000
    39 
    40 # Maximum lifetime of a connection (before being replaced with a new one) - 30min
    41 # Prevents stale connections
    42 spring.datasource.hikari.max-lifetime=1800000
    43 
    44 
    45 # JWT Configuration (supports JWT_SECRET or JWT_CONFIG_SECRET from .env)
    46 jwt.secret=${JWT_SECRET:${JWT_CONFIG_SECRET:dev-fallback-secret-min-32-chars-required}}
    47 
    48 # Base API path
    49 api.base.path=/api
    50 api.admin.path=/api/admin
    51 
    52 #spring.profiles.active=dev
    53 
    54 # Max file size
    55 server.tomcat.max-part-count=50
    56 
    57 logging.level.org.springframework.web=DEBUG
    58 logging.level.org.apache.coyote.http11=DEBUG
    59 
    60 
    61 # PostgreSQL connection
    62 spring.datasource.driver-class-name=org.postgresql.Driver
    63 spring.datasource.url=${SPRING_DATASOURCE_URL}
    64 spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
    65 spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
    66 
  • backend/src/main/java/com/trekr/backend/security/UserPrincipal.java

    r140d098 r3ebe47c  
    22
    33import lombok.Getter;
    4 import lombok.RequiredArgsConstructor;
    54
    65import java.io.Serializable;
     
    1110 */
    1211@Getter
    13 @RequiredArgsConstructor
    1412public class UserPrincipal implements Serializable {
     13    private static final long serialVersionUID = 1L;
     14
    1515    private final Long userId;
    1616    private final String username;
    1717    private final String email;
     18
     19    public UserPrincipal(Long userId, String username, String email) {
     20        this.userId = userId;
     21        this.username = username;
     22        this.email = email;
     23    }
    1824}
  • backend/src/main/java/com/trekr/backend/service/AuthService.java

    r140d098 r3ebe47c  
    66import com.trekr.backend.entity.User;
    77import com.trekr.backend.repository.UserRepository;
    8 import lombok.RequiredArgsConstructor;
    98import org.springframework.security.crypto.password.PasswordEncoder;
    109import org.springframework.stereotype.Service;
     
    1211
    1312@Service
    14 @RequiredArgsConstructor
    1513public class AuthService {
    1614
     
    1816    private final PasswordEncoder passwordEncoder;
    1917    private final JwtService jwtService;
     18
     19    public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtService jwtService) {
     20        this.userRepository = userRepository;
     21        this.passwordEncoder = passwordEncoder;
     22        this.jwtService = jwtService;
     23    }
    2024
    2125    @Transactional
  • backend/src/main/resources/application.properties

    r140d098 r3ebe47c  
    1 spring.application.name=backend
     1spring.application.name=trekr
     2
     3# ============================
     4# JPA & Hibernate
     5# ============================
     6spring.jpa.hibernate.ddl-auto=update
     7spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
     8spring.jpa.show-sql=false
     9spring.jpa.properties.hibernate.format_sql=true
     10spring.jpa.properties.hibernate.default_schema=trekr
     11spring.jpa.properties.hibernate.hbm2ddl.create_namespaces=true
     12
     13# ============================
     14# PostgreSQL connection (loaded from backend/.env or backend/env)
     15# ============================
     16spring.datasource.driver-class-name=org.postgresql.Driver
     17spring.datasource.url=${SPRING_DATASOURCE_URL}
     18spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
     19spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
     20
     21# ============================
     22# JWT Configuration (supports JWT_SECRET or JWT_CONFIG_SECRET)
     23# ============================
     24jwt.secret=${JWT_SECRET:${JWT_CONFIG_SECRET:dev-fallback-secret-min-32-chars-required}}
     25
     26# ============================
     27# Logging
     28# ============================
     29logging.level.org.springframework.security=DEBUG
     30
     31# ============================
     32# HikariCP
     33# ============================
     34spring.datasource.hikari.pool-name=trekrHikariPool
     35spring.datasource.hikari.maximum-pool-size=20
     36spring.datasource.hikari.minimum-idle=5
     37spring.datasource.hikari.connection-timeout=30000
     38spring.datasource.hikari.idle-timeout=600000
     39spring.datasource.hikari.max-lifetime=1800000
     40
     41# Max multipart parts
     42server.tomcat.max-part-count=50
  • db-scripts/ddl.sql

    r140d098 r3ebe47c  
    2020
    2121CREATE TABLE USERS (
    22     user_id BIGINT PRIMARY KEY,
     22    user_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    2323    email TEXT NOT NULL,
    2424    username TEXT NOT NULL,
  • db-scripts/dml.sql

    r140d098 r3ebe47c  
    3232(29, 'petra@example.com', 'petra_kostova', '1234'),
    3333(30, 'vlado@example.com', 'vlado_ilievski', '1234');
     34
     35-- If USERS.user_id is an IDENTITY column, ensure its backing sequence is advanced
     36-- past the inserted dummy ids so future inserts (e.g., app registration) don't
     37-- try to reuse ids and fail with duplicate key errors.
     38SELECT setval(
     39        pg_get_serial_sequence('trekr.users', 'user_id'),
     40        (SELECT COALESCE(MAX(user_id), 0) FROM trekr.users)
     41);
    3442
    3543INSERT INTO FINANCE_USERS (user_id, spending_budget, saving_budget, investing_budget, donation_budget, credit) VALUES
  • frontend/src/App.jsx

    r140d098 r3ebe47c  
     1import { Navigate, Route, Routes } from "react-router-dom";
     2
    13import LandingPage from "./pages/LandingPage/LandingPage.jsx";
     4import Login from "./pages/Login/Login.jsx";
     5import Register from "./pages/Register/Register.jsx";
     6import Dashboard from "./pages/Dashboard.jsx";
     7
     8function RequireAuth({ children }) {
     9  const token = localStorage.getItem("authToken");
     10  if (!token) return <Navigate to="/login" replace />;
     11  return children;
     12}
    213
    314function App() {
    4   return <LandingPage />;
     15  return (
     16    <div data-theme="forest" className="min-h-screen w-full">
     17      <Routes>
     18        <Route path="/" element={<LandingPage />} />
     19        <Route path="/login" element={<Login />} />
     20        <Route path="/register" element={<Register />} />
     21        <Route
     22          path="/dashboard"
     23          element={
     24            <RequireAuth>
     25              <Dashboard />
     26            </RequireAuth>
     27          }
     28        />
     29        <Route path="*" element={<Navigate to="/" replace />} />
     30      </Routes>
     31    </div>
     32  );
    533}
    634
  • frontend/src/api/axios.js

    r140d098 r3ebe47c  
    11import axios from "axios";
    22
    3 export default axios.create({
    4   baseURL: "http://localhost:8080/api",
    5   withCredentials: true, // important if using cookies
     3const api = axios.create({
     4  baseURL: import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8080/api",
    65});
     6
     7api.interceptors.request.use((config) => {
     8  const token = localStorage.getItem("authToken");
     9  if (token) {
     10    config.headers = config.headers ?? {};
     11    config.headers.Authorization = `Bearer ${token}`;
     12  }
     13  return config;
     14});
     15
     16export default api;
  • frontend/src/main.jsx

    r140d098 r3ebe47c  
    1 import { StrictMode } from 'react'
    2 import { createRoot } from 'react-dom/client'
    3 import './index.css'
    4 import App from './App.jsx'
     1import { StrictMode } from "react";
     2import { createRoot } from "react-dom/client";
     3import { BrowserRouter } from "react-router-dom";
    54
    6 createRoot(document.getElementById('root')).render(
     5import "./index.css";
     6import App from "./App.jsx";
     7
     8createRoot(document.getElementById("root")).render(
    79  <StrictMode>
    8     <App />
     10    <BrowserRouter>
     11      <App />
     12    </BrowserRouter>
    913  </StrictMode>,
    10 )
     14);
  • frontend/src/pages/Dashboard.jsx

    r140d098 r3ebe47c  
    1 // hello world
    2 import React from "react";
     1import React, { useMemo } from "react";
     2import { Link, useNavigate } from "react-router-dom";
     3
     4const Dashboard = () => {
     5  const navigate = useNavigate();
     6
     7  const user = useMemo(() => {
     8    try {
     9      const raw = localStorage.getItem("authUser");
     10      return raw ? JSON.parse(raw) : null;
     11    } catch {
     12      return null;
     13    }
     14  }, []);
     15
     16  const onLogout = () => {
     17    localStorage.removeItem("authToken");
     18    localStorage.removeItem("authUser");
     19    navigate("/", { replace: true });
     20  };
     21
     22  return (
     23    <div className="min-h-screen p-6">
     24      <div className="max-w-3xl mx-auto">
     25        <div className="flex items-center justify-between gap-3">
     26          <h1 className="text-2xl font-bold">Dashboard</h1>
     27          <div className="flex items-center gap-2">
     28            <Link className="btn btn-ghost" to="/">
     29              Landing
     30            </Link>
     31            <button className="btn btn-outline" onClick={onLogout}>
     32              Logout
     33            </button>
     34          </div>
     35        </div>
     36
     37        <div className="mt-6 card bg-base-200 border border-base-300">
     38          <div className="card-body">
     39            <h2 className="card-title">Account</h2>
     40            <div className="text-sm leading-7">
     41              <div>
     42                <span className="opacity-70">Username:</span>{" "}
     43                <span className="font-medium">{user?.username ?? "—"}</span>
     44              </div>
     45              <div>
     46                <span className="opacity-70">Email:</span>{" "}
     47                <span className="font-medium">{user?.email ?? "—"}</span>
     48              </div>
     49              <div>
     50                <span className="opacity-70">User ID:</span>{" "}
     51                <span className="font-medium">{user?.userId ?? "—"}</span>
     52              </div>
     53            </div>
     54          </div>
     55        </div>
     56      </div>
     57    </div>
     58  );
     59};
     60
     61export default Dashboard;
  • frontend/src/pages/LandingPage/components/Hero.jsx

    r140d098 r3ebe47c  
    1 import React from "react";
     1import React, { useEffect, useState } from "react";
     2import { Link } from "react-router-dom";
    23
    34const Hero = () => {
     5  const [isAuthed, setIsAuthed] = useState(() =>
     6    Boolean(localStorage.getItem("authToken")),
     7  );
     8
     9  useEffect(() => {
     10    const onStorage = (e) => {
     11      if (e.key === "authToken") {
     12        setIsAuthed(Boolean(e.newValue));
     13      }
     14    };
     15    window.addEventListener("storage", onStorage);
     16    return () => window.removeEventListener("storage", onStorage);
     17  }, []);
     18
    419  return (
    520    <div
     
    2136            consistently, and see your progress in one place.
    2237          </p>
    23           <button className="btn btn-primary">Get started</button>
     38          <Link
     39            className="btn btn-primary"
     40            to={isAuthed ? "/dashboard" : "/register"}
     41          >
     42            {isAuthed ? "Go to dashboard" : "Get started"}
     43          </Link>
    2444        </div>
    2545      </div>
  • frontend/src/pages/LandingPage/components/NavbarLanding.jsx

    r140d098 r3ebe47c  
    1 import React from "react";
     1import React, { useEffect, useState } from "react";
     2import { Link, useNavigate } from "react-router-dom";
    23import logo from "../../../assets/logo.png";
    34
    45const NavbarLanding = () => {
     6  const navigate = useNavigate();
     7  const [isAuthed, setIsAuthed] = useState(() =>
     8    Boolean(localStorage.getItem("authToken")),
     9  );
     10
     11  useEffect(() => {
     12    const onStorage = (e) => {
     13      if (e.key === "authToken") {
     14        setIsAuthed(Boolean(e.newValue));
     15      }
     16    };
     17    window.addEventListener("storage", onStorage);
     18    return () => window.removeEventListener("storage", onStorage);
     19  }, []);
     20
     21  const onLogout = () => {
     22    localStorage.removeItem("authToken");
     23    localStorage.removeItem("authUser");
     24    setIsAuthed(false);
     25    navigate("/", { replace: true });
     26  };
     27
    528  return (
    629    <div className="navbar bg-neutral shadow-sm">
    730      <div className="navbar-start">
    8         <a className="btn btn-ghost" aria-label="Trekr home">
     31        <Link className="btn btn-ghost" aria-label="Trekr home" to="/">
    932          <img src={logo} alt="Trekr" className="h-12 w-auto rounded-2xl" />
    10         </a>
     33        </Link>
    1134      </div>
    1235      <div className="navbar-end">
    13         <button className="btn bg-black text-green-200 border-black hover:bg-black/90 hover:border-black px-8 mr-6">
    14           Start Tracking
    15         </button>
     36        {isAuthed ? (
     37          <div className="flex items-center gap-2 mr-6">
     38            <Link className="btn btn-primary" to="/dashboard">
     39              Dashboard
     40            </Link>
     41            <button className="btn btn-outline" onClick={onLogout}>
     42              Logout
     43            </button>
     44          </div>
     45        ) : (
     46          <Link
     47            className="btn bg-black text-green-200 border-black hover:bg-black/90 hover:border-black px-8 mr-6"
     48            to="/login"
     49          >
     50            Start Tracking
     51          </Link>
     52        )}
    1653      </div>
    1754    </div>
Note: See TracChangeset for help on using the changeset viewer.