Index: pom.xml
===================================================================
--- pom.xml	(revision 30a24d5fe31d77745e8f6a188298f13a99c1f4d4)
+++ pom.xml	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 	<modelVersion>4.0.0</modelVersion>
 	<parent>
@@ -31,4 +32,9 @@
 	</properties>
 	<dependencies>
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-lang3</artifactId>
+			<version>3.18.0</version>
+		</dependency>
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
@@ -78,5 +84,36 @@
 			<scope>test</scope>
 		</dependency>
-	</dependencies>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context-support</artifactId>
+            <version>6.1.10</version>
+        </dependency>
+
+		<dependency>
+			<groupId>org.springdoc</groupId>
+			<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+			<version>2.5.0</version>
+		</dependency>
+
+		<!-- JWT (JJWT) -->
+		<dependency>
+			<groupId>io.jsonwebtoken</groupId>
+			<artifactId>jjwt-api</artifactId>
+			<version>0.12.5</version>
+		</dependency>
+		<dependency>
+			<groupId>io.jsonwebtoken</groupId>
+			<artifactId>jjwt-impl</artifactId>
+			<version>0.12.5</version>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>io.jsonwebtoken</groupId>
+			<artifactId>jjwt-jackson</artifactId>
+			<version>0.12.5</version>
+			<scope>runtime</scope>
+		</dependency>
+
+    </dependencies>
 
 	<build>
Index: src/main/java/mk/ukim/finki/synergymed/SynergyMedApplication.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/SynergyMedApplication.java	(revision 30a24d5fe31d77745e8f6a188298f13a99c1f4d4)
+++ src/main/java/mk/ukim/finki/synergymed/SynergyMedApplication.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -3,4 +3,9 @@
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
 
 @SpringBootApplication
@@ -11,3 +16,12 @@
 	}
 
+	@Bean
+	public PasswordEncoder passwordEncoder() {
+		return new BCryptPasswordEncoder();
+	}
+
+	@Bean
+	public AuthenticationManager authenticationManager(AuthenticationConfiguration cfg) throws Exception {
+		return cfg.getAuthenticationManager();
+	}
 }
Index: src/main/java/mk/ukim/finki/synergymed/config/ConfigProbe.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/config/ConfigProbe.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/java/mk/ukim/finki/synergymed/config/ConfigProbe.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,12 @@
+package mk.ukim.finki.synergymed.config;
+
+import org.springframework.stereotype.Component;
+
+@Component
+class ConfigProbe {
+    ConfigProbe(org.springframework.core.env.Environment env) {
+        String present = env.containsProperty("security.jwt.secret") ? "YES" : "NO";
+        System.out.println(">> security.jwt.secret present? " + present);
+    }
+}
+
Index: src/main/java/mk/ukim/finki/synergymed/config/OpenApiConfig.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/config/OpenApiConfig.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/java/mk/ukim/finki/synergymed/config/OpenApiConfig.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,29 @@
+package mk.ukim.finki.synergymed.config;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class OpenApiConfig {
+
+    @Bean
+    public OpenAPI api() {
+        final String BEARER = "bearerAuth";
+        return new OpenAPI()
+                .info(new Info().title("SynergyMed API").version("v1"))
+                .addSecurityItem(new SecurityRequirement().addList(BEARER))
+                .components(new io.swagger.v3.oas.models.Components()
+                        .addSecuritySchemes(BEARER,
+                                new SecurityScheme()
+                                        .name(BEARER)
+                                        .type(SecurityScheme.Type.HTTP)
+                                        .scheme("bearer")
+                                        .bearerFormat("JWT")
+                        )
+                );
+    }
+}
Index: src/main/java/mk/ukim/finki/synergymed/config/SecurityConfig.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/config/SecurityConfig.java	(revision 30a24d5fe31d77745e8f6a188298f13a99c1f4d4)
+++ src/main/java/mk/ukim/finki/synergymed/config/SecurityConfig.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -1,20 +1,74 @@
 package mk.ukim.finki.synergymed.config;
 
+import mk.ukim.finki.synergymed.security.JwtAuthFilter;
+import org.springframework.boot.autoconfigure.security.reactive.PathRequest;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+import java.util.List;
 
 @Configuration
+@EnableMethodSecurity
 public class SecurityConfig {
 
+    private final JwtAuthFilter jwtAuthFilter;
+
+    public SecurityConfig(JwtAuthFilter jwtAuthFilter) {
+        this.jwtAuthFilter = jwtAuthFilter;
+    }
+
     @Bean
-    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         http
-                .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
-                .csrf(csrf -> csrf.disable())   // disable CSRF if not needed
-                .formLogin(form -> form.disable()) // disable form login
-                .httpBasic(basic -> basic.disable()); // disable basic auth
+                .cors(Customizer.withDefaults()) // enable CORS using the bean below
+                .csrf(AbstractHttpConfigurer::disable)
+                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+                .authorizeHttpRequests(reg -> reg
+                        .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
+
+                        // Static resources in /static, /public, /resources, /META-INF/resources
+                        .requestMatchers(String.valueOf(PathRequest.toStaticResources().atCommonLocations())).permitAll()
+                        .requestMatchers("/", "/login", "/login.html", "/index.html", "/favicon.ico").permitAll()
+
+                        // Public API/docs
+                        .requestMatchers(
+                                "/auth/login",
+                                "/error",
+                                "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**",
+                                "/swagger-resources/**", "/webjars/**"
+                        ).permitAll()
+
+                        // Everything else requires JWT
+                        .anyRequest().authenticated()
+                )
+                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
+
         return http.build();
     }
+
+    @Bean
+    public CorsConfigurationSource corsConfigurationSource() {
+        CorsConfiguration c = new CorsConfiguration();
+        // be strict in prod (e.g., setAllowedOrigins(List.of("http://localhost:8080")))
+        c.setAllowedOriginPatterns(List.of("*"));
+        c.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
+        c.setAllowedHeaders(List.of("Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With"));
+        c.setExposedHeaders(List.of("Authorization"));
+        c.setAllowCredentials(true);
+
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", c);
+        return source;
+    }
 }
Index: src/main/java/mk/ukim/finki/synergymed/exceptions/EmailAlreadyExistsException.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/exceptions/EmailAlreadyExistsException.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/java/mk/ukim/finki/synergymed/exceptions/EmailAlreadyExistsException.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,4 @@
+package mk.ukim.finki.synergymed.exceptions;
+
+public class EmailAlreadyExistsException extends RuntimeException{
+}
Index: src/main/java/mk/ukim/finki/synergymed/exceptions/InvalidArgumentsException.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/exceptions/InvalidArgumentsException.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/java/mk/ukim/finki/synergymed/exceptions/InvalidArgumentsException.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,4 @@
+package mk.ukim.finki.synergymed.exceptions;
+
+public class InvalidArgumentsException extends RuntimeException{
+}
Index: src/main/java/mk/ukim/finki/synergymed/exceptions/InvalidEmailFormatException.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/exceptions/InvalidEmailFormatException.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/java/mk/ukim/finki/synergymed/exceptions/InvalidEmailFormatException.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,4 @@
+package mk.ukim.finki.synergymed.exceptions;
+
+public class InvalidEmailFormatException extends RuntimeException{
+}
Index: src/main/java/mk/ukim/finki/synergymed/exceptions/PasswordsMismatchException.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/exceptions/PasswordsMismatchException.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/java/mk/ukim/finki/synergymed/exceptions/PasswordsMismatchException.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,4 @@
+package mk.ukim.finki.synergymed.exceptions;
+
+public class PasswordsMismatchException extends RuntimeException{
+}
Index: src/main/java/mk/ukim/finki/synergymed/exceptions/UsernameAlreadyExistsException.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/exceptions/UsernameAlreadyExistsException.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/java/mk/ukim/finki/synergymed/exceptions/UsernameAlreadyExistsException.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,4 @@
+package mk.ukim.finki.synergymed.exceptions;
+
+public class UsernameAlreadyExistsException extends RuntimeException{
+}
Index: src/main/java/mk/ukim/finki/synergymed/models/Admin.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/models/Admin.java	(revision 30a24d5fe31d77745e8f6a188298f13a99c1f4d4)
+++ src/main/java/mk/ukim/finki/synergymed/models/Admin.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -11,5 +11,4 @@
 public class Admin {
     @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "user_id", nullable = false)
     private Integer id;
Index: src/main/java/mk/ukim/finki/synergymed/models/Client.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/models/Client.java	(revision 30a24d5fe31d77745e8f6a188298f13a99c1f4d4)
+++ src/main/java/mk/ukim/finki/synergymed/models/Client.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -11,5 +11,4 @@
 public class Client {
     @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "user_id", nullable = false)
     private Integer id;
Index: src/main/java/mk/ukim/finki/synergymed/models/Pharmacist.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/models/Pharmacist.java	(revision 30a24d5fe31d77745e8f6a188298f13a99c1f4d4)
+++ src/main/java/mk/ukim/finki/synergymed/models/Pharmacist.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -11,5 +11,4 @@
 public class Pharmacist {
     @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "user_id", nullable = false)
     private Integer id;
Index: src/main/java/mk/ukim/finki/synergymed/models/User.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/models/User.java	(revision 30a24d5fe31d77745e8f6a188298f13a99c1f4d4)
+++ src/main/java/mk/ukim/finki/synergymed/models/User.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -4,6 +4,10 @@
 import lombok.Getter;
 import lombok.Setter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
 
 import java.time.LocalDate;
+import java.util.Collection;
+import java.util.List;
 
 @Getter
@@ -11,5 +15,5 @@
 @Entity
 @Table(name = "users", schema = "synergymed")
-public class User {
+public class User implements UserDetails{
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -30,5 +34,5 @@
 
     @Column(name = "e_mail", nullable = false)
-    private String eMail;
+    private String email;
 
     @Column(name = "gender", length = 50)
@@ -41,3 +45,34 @@
     private LocalDate dateOfBirth;
 
+    private boolean isAccountNonExpired = true;
+    private boolean isAccountNonLocked = true;
+    private boolean isCredentialsNonExpired = true;
+    private boolean isEnabled = true;
+    @Override
+    public boolean isAccountNonExpired() {
+        return isAccountNonExpired;
+    }
+
+    @Override
+    public boolean isAccountNonLocked() {
+        return isAccountNonLocked;
+    }
+
+    @Override
+    public boolean isCredentialsNonExpired() {
+        return isCredentialsNonExpired;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return isEnabled;
+    }
+
+    @Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(); }
+
+    @Override public String getUsername() { return username; }
+    @Override
+    public String getPassword() {
+        return hashedPassword;
+    }
 }
Index: src/main/java/mk/ukim/finki/synergymed/repositories/UserRepository.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/repositories/UserRepository.java	(revision 30a24d5fe31d77745e8f6a188298f13a99c1f4d4)
+++ src/main/java/mk/ukim/finki/synergymed/repositories/UserRepository.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -3,5 +3,11 @@
 import mk.ukim.finki.synergymed.models.User;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.query.Param;
+
+import java.util.Optional;
 
 public interface UserRepository extends JpaRepository<User, Integer> {
+    Optional<User> findByUsername(String username);
+    Optional<User> findByEmail(String email);
+    boolean existsByUsername(String username);
 }
Index: src/main/java/mk/ukim/finki/synergymed/security/AppUserDetails.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/security/AppUserDetails.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/java/mk/ukim/finki/synergymed/security/AppUserDetails.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,38 @@
+package mk.ukim.finki.synergymed.security;
+
+import lombok.RequiredArgsConstructor;
+import mk.ukim.finki.synergymed.models.User;
+import mk.ukim.finki.synergymed.repositories.*;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.*;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+
+@Service
+@RequiredArgsConstructor
+public class AppUserDetails implements UserDetailsService {
+    private final UserRepository users;
+    private final AdminRepository admins;
+    private final PharmacistRepository pharmacists;
+    private final ClientRepository clients;
+
+    @Override
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+        User u = users.findByUsername(username)
+                .orElseThrow(() -> new UsernameNotFoundException("Not found: " + username));
+        if (!u.isEnabled()) throw new RuntimeException("Verify/enable user first.");
+
+        var auths = new ArrayList<GrantedAuthority>();
+        if (admins.existsById(u.getId()))      auths.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
+        if (pharmacists.existsById(u.getId())) auths.add(new SimpleGrantedAuthority("ROLE_PHARMACIST"));
+        if (clients.existsById(u.getId()))     auths.add(new SimpleGrantedAuthority("ROLE_CLIENT"));
+        if (auths.isEmpty())                   auths.add(new SimpleGrantedAuthority("ROLE_USER"));
+
+        return new org.springframework.security.core.userdetails.User(
+                u.getUsername(), u.getPassword(), u.isEnabled(),
+                u.isAccountNonExpired(), u.isCredentialsNonExpired(), u.isAccountNonLocked(), auths
+        );
+    }
+}
Index: src/main/java/mk/ukim/finki/synergymed/security/JwtAuthFilter.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/security/JwtAuthFilter.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/java/mk/ukim/finki/synergymed/security/JwtAuthFilter.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,57 @@
+package mk.ukim.finki.synergymed.security;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.lang.NonNull;
+import mk.ukim.finki.synergymed.service.jwt.JwtService;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Component
+public class JwtAuthFilter extends OncePerRequestFilter {
+
+    private final JwtService jwtService;
+    private final UserDetailsService userDetailsService; // your AppUserDetails bean
+
+    public JwtAuthFilter(JwtService jwtService, UserDetailsService userDetailsService){
+        this.jwtService = jwtService;
+        this.userDetailsService = userDetailsService;
+    }
+
+    @Override
+    protected void doFilterInternal(@NonNull HttpServletRequest req,
+                                    @NonNull HttpServletResponse res,
+                                    @NonNull FilterChain chain)
+            throws ServletException, IOException {
+
+        String header = req.getHeader("Authorization");
+        if (header != null && header.startsWith("Bearer ")) {
+            String token = header.substring(7);
+            try {
+                var claims = jwtService.parse(token).getPayload();
+                var username = claims.getSubject();
+
+                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+                    UserDetails ud = userDetailsService.loadUserByUsername(username);
+                    var auth = new UsernamePasswordAuthenticationToken(ud, null, ud.getAuthorities());
+                    auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
+                    SecurityContextHolder.getContext().setAuthentication(auth);
+                }
+
+                // TODO: 28.8.2025 HANDLE INVALID JWT TOKEN PROPERLY
+            } catch (Exception ignored) {
+                // invalid/expired token -> leave context unauthenticated
+            }
+        }
+        chain.doFilter(req, res);
+    }
+}
Index: src/main/java/mk/ukim/finki/synergymed/service/jwt/JwtService.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/service/jwt/JwtService.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/java/mk/ukim/finki/synergymed/service/jwt/JwtService.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,56 @@
+package mk.ukim.finki.synergymed.service.jwt;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.List;
+
+@Component
+public class JwtService {
+    private final SecretKey key;     // <— SecretKey
+    private final long expMillis;
+
+    public JwtService(
+            @Value("${security.jwt.secret:${SECURITY_JWT_SECRET:}}") String secret,
+            @Value("${security.jwt.exp-min:${SECURITY_JWT_EXP_MIN:60}}") long expMin
+    ) {
+        if (secret == null || secret.length() < 32) {
+            throw new IllegalStateException("security.jwt.secret (or env SECURITY_JWT_SECRET) must be ≥ 32 chars");
+        }
+        this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+        this.expMillis = expMin * 60_000;
+    }
+
+    public String generate(UserDetails ud) {
+        Date now = new Date();
+        Date exp = new Date(now.getTime() + expMillis);
+        List<String> roles = ud.getAuthorities().stream()
+                .map(GrantedAuthority::getAuthority)
+                .toList();
+
+        return Jwts.builder()
+                .subject(ud.getUsername())     // 0.12.x
+                .claim("roles", roles)
+                .issuedAt(now)
+                .expiration(exp)
+                .signWith(key)                 // <— let JJWT infer HS256/384/512 from key size
+                .compact();
+    }
+
+    public Jws<Claims> parse(String token) {
+        return Jwts.parser()               // 0.12.x
+                .verifyWith(key)
+                .build()
+                .parseSignedClaims(token);
+    }
+}
Index: src/main/java/mk/ukim/finki/synergymed/web/HomeController.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/web/HomeController.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/java/mk/ukim/finki/synergymed/web/HomeController.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,4 @@
+package mk.ukim.finki.synergymed.web;
+
+public class HomeController {
+}
Index: src/main/java/mk/ukim/finki/synergymed/web/LoginController.java
===================================================================
--- src/main/java/mk/ukim/finki/synergymed/web/LoginController.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/java/mk/ukim/finki/synergymed/web/LoginController.java	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,59 @@
+package mk.ukim.finki.synergymed.web;
+
+import lombok.RequiredArgsConstructor;
+import mk.ukim.finki.synergymed.service.jwt.JwtService;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+
+import jakarta.servlet.http.HttpSession;
+
+@Controller
+@RequestMapping("/login")
+@RequiredArgsConstructor
+public class LoginController {
+
+    private final AuthenticationManager authenticationManager;
+    private final JwtService jwtService;
+
+    @GetMapping
+    public String getLoginPage() {
+        return "login";
+    }
+
+    @PostMapping
+    public String login(@RequestParam String username,
+                        @RequestParam String password,
+                        HttpSession session,
+                        Model model) {
+        try {
+            var authentication = authenticationManager.authenticate(
+                    new UsernamePasswordAuthenticationToken(username, password)
+            );
+
+            var userDetails = (UserDetails) authentication.getPrincipal();
+            String token = jwtService.generate(userDetails);
+
+            // store jwt in session
+            session.setAttribute("jwt_token", token);
+            session.setAttribute("username", username);
+            session.setAttribute("user", userDetails);
+
+            // redirect to home after a successful login
+            return "redirect:/home";
+
+        } catch (BadCredentialsException e) {
+            model.addAttribute("error", "Invalid username or password");
+            model.addAttribute("username", username);
+            return "login";
+        } catch (Exception e) {
+            model.addAttribute("error", "An error occurred during login");
+            model.addAttribute("username", username);
+            return "login";
+        }
+    }
+}
Index: src/main/resources/application.properties
===================================================================
--- src/main/resources/application.properties	(revision 30a24d5fe31d77745e8f6a188298f13a99c1f4d4)
+++ src/main/resources/application.properties	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -7,4 +7,9 @@
 spring.jpa.properties.hibernate.default_schema=synergymed
 spring.datasource.driver-class-name=org.postgresql.Driver
+
+security.jwt.secret=REPLACE_WITH_A_RANDOM_32+_CHAR_SECRET_KEY_1234567890abcdef
+security.jwt.exp-min=60
+
+spring.datasource.hikari.maximum-pool-size=5
 
 # JPA / Hibernate settings
@@ -17,2 +22,5 @@
 spring.servlet.multipart.max-file-size=5MB
 spring.servlet.multipart.max-request-size=10MB
+
+spring.docker.compose.enabled=false
+
Index: src/main/resources/templates/login.html
===================================================================
--- src/main/resources/templates/login.html	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
+++ src/main/resources/templates/login.html	(revision 6c7d5940029eb1a4e177c09de774b329c0e47272)
@@ -0,0 +1,284 @@
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>SynergyMed - Login</title>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+      box-sizing: border-box;
+    }
+
+    body {
+      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+      background: linear-gradient(135deg, #a4ecba 0%, #fefeff 100%);
+      min-height: 100vh;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding: 20px;
+    }
+
+    .login-container {
+      background: white;
+      border-radius: 20px;
+      box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
+      overflow: hidden;
+      width: 100%;
+      max-width: 400px;
+      backdrop-filter: blur(10px);
+    }
+
+    .login-header {
+      background: linear-gradient(135deg, #20b2aa, #48d1cc);
+      padding: 40px 30px;
+      text-align: center;
+      color: white;
+    }
+
+    .login-header h1 {
+      font-size: 2rem;
+      font-weight: 300;
+      margin-bottom: 8px;
+    }
+
+    .login-header p {
+      opacity: 0.9;
+      font-size: 0.9rem;
+    }
+
+    .login-form {
+      padding: 40px 30px;
+    }
+
+    .form-group {
+      margin-bottom: 25px;
+      position: relative;
+    }
+
+    .form-group label {
+      display: block;
+      margin-bottom: 8px;
+      color: #555;
+      font-weight: 500;
+      font-size: 0.9rem;
+    }
+
+    .form-control {
+      width: 100%;
+      padding: 15px 20px;
+      border: 2px solid #e1e5e9;
+      border-radius: 12px;
+      font-size: 1rem;
+      transition: all 0.3s ease;
+      background: #f8f9fa;
+    }
+
+    .form-control:focus {
+      outline: none;
+      border-color: #20b2aa;
+      background: white;
+      box-shadow: 0 0 0 3px rgba(32, 178, 170, 0.1);
+    }
+
+    .btn-login {
+      width: 100%;
+      padding: 15px;
+      background: linear-gradient(135deg, #20b2aa, #48d1cc);
+      color: white;
+      border: none;
+      border-radius: 12px;
+      font-size: 1rem;
+      font-weight: 600;
+      cursor: pointer;
+      transition: all 0.3s ease;
+      text-transform: uppercase;
+      letter-spacing: 1px;
+    }
+
+    .btn-login:hover {
+      transform: translateY(-2px);
+      box-shadow: 0 10px 20px rgba(32, 178, 170, 0.3);
+    }
+
+    .btn-login:active {
+      transform: translateY(0);
+    }
+
+    .alert {
+      padding: 15px;
+      border-radius: 8px;
+      margin-bottom: 20px;
+      font-size: 0.9rem;
+    }
+
+    .alert-danger {
+      background-color: #fee;
+      color: #c33;
+      border: 1px solid #fcc;
+    }
+
+    .alert-success {
+      background-color: #efe;
+      color: #363;
+      border: 1px solid #cfc;
+    }
+
+    .forgot-password {
+      text-align: center;
+      margin-top: 20px;
+    }
+
+    .forgot-password a {
+      color: #20b2aa;
+      text-decoration: none;
+      font-size: 0.9rem;
+      transition: color 0.3s ease;
+    }
+
+    .forgot-password a:hover {
+      color: #1a9999;
+      text-decoration: underline;
+    }
+
+    .loading {
+      display: none;
+      position: relative;
+    }
+
+    .loading::after {
+      content: '';
+      position: absolute;
+      width: 20px;
+      height: 20px;
+      margin: auto;
+      border: 2px solid transparent;
+      border-top-color: #ffffff;
+      border-radius: 50%;
+      animation: spin 1s linear infinite;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+    }
+
+    @keyframes spin {
+      0% { transform: translate(-50%, -50%) rotate(0deg); }
+      100% { transform: translate(-50%, -50%) rotate(360deg); }
+    }
+
+    .form-footer {
+      text-align: center;
+      padding: 20px 30px;
+      background-color: #f8f9fa;
+      border-top: 1px solid #e9ecef;
+    }
+
+    .form-footer p {
+      color: #6c757d;
+      font-size: 0.85rem;
+    }
+
+    @media (max-width: 480px) {
+      .login-container {
+        margin: 10px;
+        border-radius: 15px;
+      }
+
+      .login-header {
+        padding: 30px 20px;
+      }
+
+      .login-form {
+        padding: 30px 20px;
+      }
+
+      .login-header h1 {
+        font-size: 1.7rem;
+      }
+    }
+  </style>
+</head>
+<body>
+<div class="login-container">
+  <div class="login-header">
+    <h1>SynergyMed</h1>
+    <p>Welcome back! Please sign in to your account.</p>
+  </div>
+
+  <div class="login-form">
+    <!-- Error message -->
+    <div th:if="${error}" class="alert alert-danger">
+      <span th:text="${error}">Invalid credentials</span>
+    </div>
+
+    <!-- Success message -->
+    <div th:if="${message}" class="alert alert-success">
+      <span th:text="${message}">Success message</span>
+    </div>
+
+    <form th:action="@{/login}" method="post" id="loginForm">
+      <div class="form-group">
+        <label for="username">Username</label>
+        <input type="text"
+               id="username"
+               name="username"
+               class="form-control"
+               th:value="${username}"
+               placeholder="Enter your username"
+               required>
+      </div>
+
+      <div class="form-group">
+        <label for="password">Password</label>
+        <input type="password"
+               id="password"
+               name="password"
+               class="form-control"
+               placeholder="Enter your password"
+               required>
+      </div>
+
+      <button type="submit" class="btn-login" id="loginBtn">
+        <span class="btn-text">Sign In</span>
+        <div class="loading"></div>
+      </button>
+    </form>
+
+    <div class="forgot-password">
+      <a href="#" th:href="@{/forgot-password}">Forgot your password?</a>
+    </div>
+  </div>
+
+  <div class="form-footer">
+    <p>&copy; 2024 SynergyMed. All rights reserved.</p>
+  </div>
+</div>
+
+<script>
+  document.getElementById('loginForm').addEventListener('submit', function() {
+    const btn = document.getElementById('loginBtn');
+    const btnText = btn.querySelector('.btn-text');
+    const loading = btn.querySelector('.loading');
+
+    btnText.style.display = 'none';
+    loading.style.display = 'block';
+    btn.disabled = true;
+  });
+
+  // Add input focus animations
+  document.querySelectorAll('.form-control').forEach(input => {
+    input.addEventListener('focus', function() {
+      this.parentElement.classList.add('focused');
+    });
+
+    input.addEventListener('blur', function() {
+      if (!this.value) {
+        this.parentElement.classList.remove('focused');
+      }
+    });
+  });
+</script>
+</body>
+</html>
