Compare commits
4 Commits
main
...
feature/au
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3641dbcdf9 | ||
![]() |
656e0c0e7a | ||
![]() |
2e0c93a400 | ||
![]() |
ba239764bf |
@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.springframework.boot' version '3.4.1'
|
id 'org.springframework.boot' version '3.3.3'
|
||||||
id 'io.spring.dependency-management' version '1.1.6'
|
id 'io.spring.dependency-management' version '1.1.6'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,8 +26,6 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
|
||||||
runtimeOnly 'org.postgresql:postgresql'
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
services:
|
|
||||||
cc-database:
|
|
||||||
image: postgres
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: biblenotes
|
|
||||||
POSTGRES_PASSWORD: ujhfdogiuhfdgiusdfhoviufdnpviusdhdfiuqbfoiudzsfzidfugofduszbdv
|
|
||||||
POSTGRES_DB: biblenotes
|
|
||||||
volumes:
|
|
||||||
- ./sql-scripts:/docker-entrypoint-initdb.d
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
@ -1,118 +0,0 @@
|
|||||||
CREATE TABLE users
|
|
||||||
(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
username VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
password VARCHAR(255) NOT NULL,
|
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
role VARCHAR(255) NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL,
|
|
||||||
updated_at TIMESTAMP NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE user_logins
|
|
||||||
(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id BIGINT NOT NULL,
|
|
||||||
login_time TIMESTAMP NOT NULL,
|
|
||||||
login_ip VARCHAR(255) NOT NULL,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE bible_reading_plans
|
|
||||||
(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
plan_name VARCHAR(255) NOT NULL,
|
|
||||||
start_date TIMESTAMP NOT NULL,
|
|
||||||
chapter_per_day SMALLINT NOT NULL CHECK (chapter_per_day > 0),
|
|
||||||
creator_user_id BIGINT NOT NULL,
|
|
||||||
FOREIGN KEY (creator_user_id) REFERENCES users (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE bible_books
|
|
||||||
(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
book_name_en VARCHAR(255) NOT NULL,
|
|
||||||
book_name_de VARCHAR(255) NOT NULL,
|
|
||||||
chapter_count INT NOT NULL CHECK (chapter_count > 0),
|
|
||||||
is_new_testament BOOLEAN NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE user_statistics
|
|
||||||
(
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id BIGINT NOT NULL,
|
|
||||||
current_book_id BIGINT,
|
|
||||||
current_chapter_id BIGINT,
|
|
||||||
bible_reading_plan_id BIGINT,
|
|
||||||
FOREIGN KEY (bible_reading_plan_id) REFERENCES bible_reading_plans (id),
|
|
||||||
FOREIGN KEY (current_chapter_id) REFERENCES bible_books (id),
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO bible_books (book_name_en, book_name_de, chapter_count, is_new_testament)
|
|
||||||
VALUES ('Genesis', '1. Mose (Genesis)', 50, FALSE),
|
|
||||||
('Exodus', '2. Mose (Exodus)', 40, FALSE),
|
|
||||||
('Leviticus', '3. Mose (Levitikus)', 27, FALSE),
|
|
||||||
('Numbers', '4. Mose (Numeri)', 36, FALSE),
|
|
||||||
('Deuteronomy', '5. Mose (Deuteronomium)', 34, FALSE),
|
|
||||||
('Joshua', 'Josua', 24, FALSE),
|
|
||||||
('Judges', 'Richter', 21, FALSE),
|
|
||||||
('Ruth', 'Ruth', 4, FALSE),
|
|
||||||
('1. Samuel', '1. Samuel', 31, FALSE),
|
|
||||||
('2. Samuel', '2. Samuel', 24, FALSE),
|
|
||||||
('1. Kings', '1. Könige', 22, FALSE),
|
|
||||||
('2. Kings', '2. Könige', 25, FALSE),
|
|
||||||
('1. Chronicles', '1. Chronik', 29, FALSE),
|
|
||||||
('2. Chronicles', '2. Chronik', 36, FALSE),
|
|
||||||
('Ezra', 'Esra', 10, FALSE),
|
|
||||||
('Nehemiah', 'Nehemia', 13, FALSE),
|
|
||||||
('Esther', 'Ester', 10, FALSE),
|
|
||||||
('Job', 'Hiob', 42, FALSE),
|
|
||||||
('Psalms', 'Psalmen', 150, FALSE),
|
|
||||||
('Proverbs', 'Sprüche', 31, FALSE),
|
|
||||||
('Ecclesiastes', 'Prediger', 12, FALSE),
|
|
||||||
('Song of Solomon', 'Hohelied', 8, FALSE),
|
|
||||||
('Isaiah', 'Jesaja', 66, FALSE),
|
|
||||||
('Jeremiah', 'Jeremia', 52, FALSE),
|
|
||||||
('Lamentations', 'Klagelieder', 5, FALSE),
|
|
||||||
('Ezekiel', 'Hesekiel', 48, FALSE),
|
|
||||||
('Daniel', 'Daniel', 12, FALSE),
|
|
||||||
('Hosea', 'Hosea', 14, FALSE),
|
|
||||||
('Joel', 'Joel', 3, FALSE),
|
|
||||||
('Amos', 'Amos', 9, FALSE),
|
|
||||||
('Obadiah', 'Obadja', 1, FALSE),
|
|
||||||
('Jonah', 'Jona', 4, FALSE),
|
|
||||||
('Micah', 'Micha', 7, FALSE),
|
|
||||||
('Nahum', 'Nahum', 3, FALSE),
|
|
||||||
('Habakkuk', 'Habakuk', 3, FALSE),
|
|
||||||
('Zephaniah', 'Zefanja', 3, FALSE),
|
|
||||||
('Haggai', 'Haggai', 2, FALSE),
|
|
||||||
('Zechariah', 'Sacharja', 14, FALSE),
|
|
||||||
('Malachi', 'Maleachi', 4, FALSE),
|
|
||||||
('Matthew', 'Matthäus', 28, TRUE),
|
|
||||||
('Mark', 'Markus', 16, TRUE),
|
|
||||||
('Luke', 'Lukas', 24, TRUE),
|
|
||||||
('John', 'Johannes', 21, TRUE),
|
|
||||||
('Acts', 'Apostelgeschichte', 28, TRUE),
|
|
||||||
('Romans', 'Römer', 16, TRUE),
|
|
||||||
('1. Corinthians', '1. Korinther', 16, TRUE),
|
|
||||||
('2. Corinthians', '2. Korinther', 13, TRUE),
|
|
||||||
('Galatians', 'Galater', 6, TRUE),
|
|
||||||
('Ephesians', 'Epheser', 6, TRUE),
|
|
||||||
('Philippians', 'Philipper', 4, TRUE),
|
|
||||||
('Colossians', 'Kolosser', 4, TRUE),
|
|
||||||
('1. Thessalonians', '1. Thessalonicher', 5, TRUE),
|
|
||||||
('2. Thessalonians', '2. Thessalonicher', 3, TRUE),
|
|
||||||
('1. Timothy', '1. Timotheus', 6, TRUE),
|
|
||||||
('2. Timothy', '2. Timotheus', 4, TRUE),
|
|
||||||
('Titus', 'Titus', 3, TRUE),
|
|
||||||
('Philemon', 'Philemon', 1, TRUE),
|
|
||||||
('Hebrews', 'Hebräer', 13, TRUE),
|
|
||||||
('James', 'Jakobus', 5, TRUE),
|
|
||||||
('1. Peter', '1. Petrus', 5, TRUE),
|
|
||||||
('2. Peter', '2. Petrus', 3, TRUE),
|
|
||||||
('1. John', '1. Johannes', 5, TRUE),
|
|
||||||
('2. John', '2. Johannes', 1, TRUE),
|
|
||||||
('3. John', '3. Johannes', 1, TRUE),
|
|
||||||
('Jude', 'Judas', 1, TRUE),
|
|
||||||
('Revelation', 'Offenbarung', 22, TRUE);
|
|
@ -6,14 +6,12 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.security.config.Customizer;
|
import org.springframework.security.config.Customizer;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
|
||||||
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@ -35,32 +33,20 @@ public class SecurityConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
|
||||||
// TODO: Fix security config for this project (currently old state from sharepulse)
|
|
||||||
|
|
||||||
http
|
http
|
||||||
.csrf(csrf -> csrf
|
.csrf(csrf -> csrf.ignoringRequestMatchers("/api/v1/**")) // Disable CSRF for API routes
|
||||||
.ignoringRequestMatchers("/**")
|
|
||||||
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) // Disable CSRF for API routes
|
|
||||||
.sessionManagement(sessionManagement -> sessionManagement
|
.sessionManagement(sessionManagement -> sessionManagement
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // No session will be created by Spring Security
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // No session will be created by Spring Security
|
||||||
)
|
)
|
||||||
.authorizeHttpRequests(authorize -> authorize
|
.authorizeHttpRequests(authorize -> authorize
|
||||||
.requestMatchers("/secure/**").authenticated() // Secure these endpoints
|
.requestMatchers("/api/v1/auth/login").permitAll() // Allow access to login endpoint
|
||||||
|
.requestMatchers("/api/v1/**").authenticated() // Secure all other /api/v1/** routes
|
||||||
.anyRequest().permitAll() // All other requests are allowed without authentication
|
.anyRequest().permitAll() // All other requests are allowed without authentication
|
||||||
)
|
)
|
||||||
.headers(headers -> headers
|
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // Ensure JWT filter is applied after login is allowed
|
||||||
.frameOptions(HeadersConfigurer.FrameOptionsConfig::deny) // Prevent clickjacking
|
.logout(LogoutConfigurer::permitAll)
|
||||||
//.contentSecurityPolicy(Customizer.withDefaults()) // Blocks loading of resources from other domains
|
.rememberMe(Customizer.withDefaults());
|
||||||
.xssProtection(Customizer.withDefaults())
|
|
||||||
)
|
|
||||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // Apply JWT filter
|
|
||||||
.logout(LogoutConfigurer::permitAll);
|
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Thoughts:
|
|
||||||
* - Instead of disabling the contentSecurityPolicy we should simply provide our own libraries so that no external cdns are needed
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
package de.w665.biblenotes.config;
|
|
||||||
|
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|
||||||
|
|
||||||
public class WebConfig implements WebMvcConfigurer {
|
|
||||||
@Override
|
|
||||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
|
||||||
// Map static resources to the root path
|
|
||||||
registry.addResourceHandler("/**")
|
|
||||||
.addResourceLocations("classpath:/static/");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package de.w665.biblenotes.db;
|
|
||||||
|
|
||||||
import de.w665.biblenotes.db.entity.Role;
|
|
||||||
import de.w665.biblenotes.db.entity.User;
|
|
||||||
import de.w665.biblenotes.db.repo.UserRepository;
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@Slf4j
|
|
||||||
public class DatabaseInitService {
|
|
||||||
|
|
||||||
@Value("${database.init.create-default-admin-user}")
|
|
||||||
private boolean createAdminUser;
|
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
|
|
||||||
public DatabaseInitService(UserRepository userRepository) {
|
|
||||||
this.userRepository = userRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void init() {
|
|
||||||
if(createAdminUser && userRepository.count() == 0) {
|
|
||||||
createDefaultAdminUser();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createDefaultAdminUser() {
|
|
||||||
log.debug("Inserting admin user...");
|
|
||||||
User user = new User();
|
|
||||||
user.setUsername("admin");
|
|
||||||
user.setPassword("$2a$10$y5u4UL6JwacDrMzXEJqqQO.1Cf3xoZTRhUTab4We/zxnWneI5jTf2");
|
|
||||||
user.setEmail("admin@example.com");
|
|
||||||
user.setRole(Role.ADMINISTRATOR);
|
|
||||||
user.setCreatedAt(LocalDateTime.now());
|
|
||||||
user.setUpdatedAt(LocalDateTime.now());
|
|
||||||
userRepository.save(user);
|
|
||||||
log.debug("Admin user insertion done.");
|
|
||||||
}
|
|
||||||
}
|
|
18
src/main/java/de/w665/biblenotes/db/RethinkDBConfig.java
Normal file
18
src/main/java/de/w665/biblenotes/db/RethinkDBConfig.java
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package de.w665.biblenotes.db;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Configuration
|
||||||
|
public class RethinkDBConfig {
|
||||||
|
@Value("${rethinkdb.host}")
|
||||||
|
private String host;
|
||||||
|
|
||||||
|
@Value("${rethinkdb.port}")
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
@Value("${rethinkdb.database}")
|
||||||
|
private String database;
|
||||||
|
}
|
27
src/main/java/de/w665/biblenotes/db/RethinkDBConnector.java
Normal file
27
src/main/java/de/w665/biblenotes/db/RethinkDBConnector.java
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package de.w665.biblenotes.db;
|
||||||
|
|
||||||
|
import com.rethinkdb.RethinkDB;
|
||||||
|
import com.rethinkdb.net.Connection;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RethinkDBConnector {
|
||||||
|
|
||||||
|
private final RethinkDBConfig config;
|
||||||
|
@Getter
|
||||||
|
private final RethinkDB r = RethinkDB.r;
|
||||||
|
@Getter
|
||||||
|
private Connection connection;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void connectToDatabae() {
|
||||||
|
connection = r.connection().hostname(config.getHost()).port(config.getPort()).connect();
|
||||||
|
log.info("Connected to RethinkDB at " + config.getHost() + ":" + config.getPort() + " on database " + config.getDatabase());
|
||||||
|
}
|
||||||
|
}
|
115
src/main/java/de/w665/biblenotes/db/RethinkDBService.java
Normal file
115
src/main/java/de/w665/biblenotes/db/RethinkDBService.java
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package de.w665.biblenotes.db;
|
||||||
|
|
||||||
|
import com.rethinkdb.RethinkDB;
|
||||||
|
import com.rethinkdb.gen.exc.ReqlOpFailedError;
|
||||||
|
import com.rethinkdb.net.Connection;
|
||||||
|
import de.w665.biblenotes.db.repo.UserRepository;
|
||||||
|
import de.w665.biblenotes.model.User;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class RethinkDBService {
|
||||||
|
|
||||||
|
private final RethinkDBConfig config;
|
||||||
|
private final RethinkDB r;
|
||||||
|
private final Connection connection;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
@Value("${biblenotes.auto-reset-on-startup}")
|
||||||
|
private boolean autoResetOnStartup;
|
||||||
|
@Value("${biblenotes.management.user.username}")
|
||||||
|
private String defaultUsername;
|
||||||
|
@Value("${biblenotes.management.user.password}")
|
||||||
|
private String defaultPassword;
|
||||||
|
|
||||||
|
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public RethinkDBService(RethinkDBConfig config, RethinkDBConnector connector, UserRepository userRepository) {
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
// mapping to private vars for easier access
|
||||||
|
this.r = connector.getR();
|
||||||
|
this.connection = connector.getConnection();
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initialize() {
|
||||||
|
|
||||||
|
// rethinkdb check if database exists
|
||||||
|
try {
|
||||||
|
r.dbCreate(config.getDatabase()).run(connection).stream();
|
||||||
|
log.debug("Database " + config.getDatabase() + " created");
|
||||||
|
} catch (ReqlOpFailedError e) {
|
||||||
|
log.debug("Database " + config.getDatabase() + " already exists. Error: " + e.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// rethinkdb check if table users exists
|
||||||
|
try {
|
||||||
|
r.db(config.getDatabase()).tableCreate("users").run(connection).stream();
|
||||||
|
log.debug("Table 'users' created successfully.");
|
||||||
|
} catch (ReqlOpFailedError e) {
|
||||||
|
log.debug("Table 'users' already exists.");
|
||||||
|
if(autoResetOnStartup) {
|
||||||
|
log.debug("Clearing content...");
|
||||||
|
r.db(config.getDatabase()).table("users").delete().run(connection);
|
||||||
|
log.debug("Table 'users' cleared successfully.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rethinkdb check if table user_logins exists
|
||||||
|
try {
|
||||||
|
r.db(config.getDatabase()).tableCreate("user_logins").run(connection).stream();
|
||||||
|
log.debug("Table 'user_logins' created successfully.");
|
||||||
|
} catch (ReqlOpFailedError e) {
|
||||||
|
log.debug("Table 'user_logins' already exists.");
|
||||||
|
if(autoResetOnStartup) {
|
||||||
|
log.debug("Clearing content...");
|
||||||
|
r.db(config.getDatabase()).table("user_logins").delete().run(connection);
|
||||||
|
log.debug("Table 'user_logins' cleared successfully.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
r.db(config.getDatabase()).table("user_logins").indexCreate("loginTime").run(connection);
|
||||||
|
log.debug("Secondary index 'loginTime' on table 'user_logins' successfully created.");
|
||||||
|
} catch (ReqlOpFailedError e) {
|
||||||
|
log.debug("Secondary index 'loginTime' already exists.");
|
||||||
|
} finally {
|
||||||
|
r.db(config.getDatabase()).table("user_logins").indexWait("loginTime").run(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeAdminUser();
|
||||||
|
|
||||||
|
log.info("Database ready for operation!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeAdminUser() {
|
||||||
|
Optional<User> adminUser = userRepository.retrieveUserByUsername("admin");
|
||||||
|
if(adminUser.isEmpty()) {
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(defaultUsername);
|
||||||
|
user.setPassword(passwordEncoder.encode(defaultPassword));
|
||||||
|
user.setRole("ADMIN");
|
||||||
|
userRepository.insertUser(user);
|
||||||
|
log.debug("Admin user created with default credentials. Username: admin, Password: admin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void close() {
|
||||||
|
if (connection != null) {
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
package de.w665.biblenotes.db.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "bible_books")
|
|
||||||
@Getter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@ToString
|
|
||||||
public class BibleBook {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "book_name_en", nullable = false)
|
|
||||||
private String bookNameEn;
|
|
||||||
|
|
||||||
@Column(name = "book_name_de", nullable = false)
|
|
||||||
private String bookNameDe;
|
|
||||||
|
|
||||||
@Column(name = "chapter_count", nullable = false)
|
|
||||||
private int chapterCount;
|
|
||||||
|
|
||||||
@Column(name = "is_new_testament", nullable = false)
|
|
||||||
private boolean isNewTestament;
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package de.w665.biblenotes.db.entity;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "bible_reading_plans")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@AllArgsConstructor
|
|
||||||
@ToString
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class BibleReadingPlan {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "plan_name", nullable = false)
|
|
||||||
private String planName;
|
|
||||||
|
|
||||||
@Column(name = "start_date", nullable = false)
|
|
||||||
private LocalDateTime startDate;
|
|
||||||
|
|
||||||
@Column(name = "chapter_per_day", nullable = false)
|
|
||||||
private short chapterPerDay;
|
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "creator_user_id", nullable = false)
|
|
||||||
private User creatorUser;
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package de.w665.biblenotes.db.entity;
|
|
||||||
|
|
||||||
public enum Role {
|
|
||||||
ADMINISTRATOR,
|
|
||||||
USER
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
package de.w665.biblenotes.db.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "users")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@ToString
|
|
||||||
public class User {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(nullable = false, unique = true)
|
|
||||||
private String username;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
@Column(nullable = false, unique = true)
|
|
||||||
private String email;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
@Column(nullable = false)
|
|
||||||
private Role role;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package de.w665.biblenotes.db.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "user_logins")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class UserLogin {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private Long userId;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private LocalDateTime loginTime;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String loginIp;
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
package de.w665.biblenotes.db.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "user_statistics")
|
|
||||||
@Getter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@ToString
|
|
||||||
public class UserStatistics {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "user_id", nullable = false)
|
|
||||||
private User user;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "current_book_id")
|
|
||||||
private BibleBook currentBook;
|
|
||||||
|
|
||||||
@Column(name = "current_chapter_id")
|
|
||||||
private Long currentChapterId;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "bible_reading_plan_id")
|
|
||||||
private BibleReadingPlan bibleReadingPlan;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package de.w665.biblenotes.db.repo;
|
|
||||||
|
|
||||||
import de.w665.biblenotes.db.entity.BibleBook;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface BibleBookRepository extends JpaRepository<BibleBook, Long> {
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package de.w665.biblenotes.db.repo;
|
|
||||||
|
|
||||||
import de.w665.biblenotes.db.entity.BibleReadingPlan;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface BibleReadingPlanRepository extends JpaRepository<BibleReadingPlan, Long> {
|
|
||||||
List<BibleReadingPlan> getBibleReadingPlanById(Long id);
|
|
||||||
}
|
|
@ -1,10 +1,67 @@
|
|||||||
package de.w665.biblenotes.db.repo;
|
package de.w665.biblenotes.db.repo;
|
||||||
|
|
||||||
import de.w665.biblenotes.db.entity.UserLogin;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import com.google.gson.Gson;
|
||||||
|
import com.rethinkdb.RethinkDB;
|
||||||
|
import com.rethinkdb.net.Connection;
|
||||||
|
import com.rethinkdb.net.Result;
|
||||||
|
import de.w665.biblenotes.db.RethinkDBConfig;
|
||||||
|
import de.w665.biblenotes.db.RethinkDBConnector;
|
||||||
|
import de.w665.biblenotes.model.UserLogin;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface UserLoginRepository extends JpaRepository<UserLogin, Long> {
|
@Slf4j
|
||||||
List<UserLogin> findByUserIdOrderByLoginTimeDesc(Long userId);
|
@Repository
|
||||||
|
public class UserLoginRepository {
|
||||||
|
private final RethinkDB r;
|
||||||
|
private final Connection connection;
|
||||||
|
private final RethinkDBConfig config;
|
||||||
|
private final String TABLE_NAME = "user_logins";
|
||||||
|
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public UserLoginRepository(RethinkDBConnector connector, RethinkDBConfig config) {
|
||||||
|
this.r = connector.getR();
|
||||||
|
this.connection = connector.getConnection();
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insertUserLogin(UserLogin userLogin) {
|
||||||
|
String uuid = r.uuid().run(connection, String.class).first();
|
||||||
|
userLogin.setId(uuid);
|
||||||
|
r.db(config.getDatabase()).table(TABLE_NAME).insert(userLogin).run(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserLogin getLastLogin(String userId) {
|
||||||
|
// Get the second most recent login (the most recent is the current one)
|
||||||
|
Result<UserLogin> result = r.db(config.getDatabase()).table(TABLE_NAME)
|
||||||
|
.orderBy().optArg("index", r.desc("loginTime"))
|
||||||
|
.filter(r.hashMap("userId", userId))
|
||||||
|
.skip(1).limit(1)
|
||||||
|
.run(connection, UserLogin.class);
|
||||||
|
// Return the second most recent login if exists
|
||||||
|
return result.hasNext() ? result.next() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserLogin> getUserLogins(String userId) {
|
||||||
|
Result<UserLogin> result = r.db(config.getDatabase()).table(TABLE_NAME)
|
||||||
|
.orderBy().optArg("index", r.desc("loginTime"))
|
||||||
|
.filter(r.hashMap("userId", userId))
|
||||||
|
.run(connection, UserLogin.class);
|
||||||
|
return result.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAllUserLogins(String userId) {
|
||||||
|
r.db(config.getDatabase()).table(TABLE_NAME)
|
||||||
|
.filter(r.hashMap("userId", userId))
|
||||||
|
.delete()
|
||||||
|
.run(connection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,55 @@
|
|||||||
package de.w665.biblenotes.db.repo;
|
package de.w665.biblenotes.db.repo;
|
||||||
|
|
||||||
import de.w665.biblenotes.db.entity.User;
|
import com.rethinkdb.RethinkDB;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import com.rethinkdb.net.Connection;
|
||||||
import org.springframework.data.jpa.repository.Modifying;
|
import de.w665.biblenotes.db.RethinkDBConfig;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import de.w665.biblenotes.db.RethinkDBConnector;
|
||||||
import org.springframework.data.repository.query.Param;
|
import de.w665.biblenotes.model.User;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
@Repository
|
||||||
import java.util.Optional;
|
public class UserRepository {
|
||||||
|
private final RethinkDB r;
|
||||||
|
private final Connection connection;
|
||||||
|
private final RethinkDBConfig config;
|
||||||
|
@Autowired
|
||||||
|
public UserRepository(RethinkDBConnector connector, RethinkDBConfig config) {
|
||||||
|
this.r = connector.getR();
|
||||||
|
this.connection = connector.getConnection();
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public Optional<User> retrieveUserByUsername(String username) {
|
||||||
Optional<User> findByUsername(String username);
|
try {
|
||||||
|
User user = r.db(config.getDatabase()).table("users")
|
||||||
|
.filter(r.hashMap("username", username))
|
||||||
|
.run(connection, User.class)
|
||||||
|
.next();
|
||||||
|
return Optional.ofNullable(user);
|
||||||
|
} catch (NoSuchElementException e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Modifying
|
public void updateLastLoginForUser(String username, Date lastLogin) {
|
||||||
@Transactional
|
r.db(config.getDatabase()).table("users")
|
||||||
@Query("UPDATE User u SET u.updatedAt = :lastLogin WHERE u.username = :username")
|
.filter(r.hashMap("username", username))
|
||||||
void updateLastLoginForUser(@Param("username") String username, @Param("lastLogin") LocalDateTime lastLogin);
|
.update(r.hashMap("lastLogin", lastLogin.getTime()))
|
||||||
|
.run(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateUser(User user) {
|
||||||
|
r.db(config.getDatabase()).table("users")
|
||||||
|
.filter(r.hashMap("id", user.getId()))
|
||||||
|
.update(user)
|
||||||
|
.run(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insertUser(User user) {
|
||||||
|
String optionalUuid = r.uuid().run(connection, String.class).first();
|
||||||
|
user.setId(optionalUuid);
|
||||||
|
r.db(config.getDatabase()).table("users").insert(user).run(connection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
package de.w665.biblenotes.db.repo;
|
|
||||||
|
|
||||||
import de.w665.biblenotes.db.entity.UserStatistics;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface UserStatisticsRepository extends JpaRepository<UserStatistics, Long> {
|
|
||||||
}
|
|
16
src/main/java/de/w665/biblenotes/model/User.java
Normal file
16
src/main/java/de/w665/biblenotes/model/User.java
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package de.w665.biblenotes.model;
|
||||||
|
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class User {
|
||||||
|
private String id; // ID is auto mapped by RethinkDB
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private String email;
|
||||||
|
private String role;
|
||||||
|
}
|
21
src/main/java/de/w665/biblenotes/model/UserLogin.java
Normal file
21
src/main/java/de/w665/biblenotes/model/UserLogin.java
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package de.w665.biblenotes.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserLogin {
|
||||||
|
String id;
|
||||||
|
String userId;
|
||||||
|
@JsonFormat(timezone = "ETC")
|
||||||
|
Date loginTime;
|
||||||
|
String loginIp;
|
||||||
|
}
|
@ -4,6 +4,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/secure")
|
@RequestMapping("/api/v1")
|
||||||
public abstract class SecureApiRestController {
|
public abstract class ApiRestController {
|
||||||
}
|
}
|
||||||
|
|
@ -1,18 +0,0 @@
|
|||||||
package de.w665.biblenotes.rest.dto;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString
|
|
||||||
public class BibleReadingPlanDTO {
|
|
||||||
private Long id;
|
|
||||||
private String planName;
|
|
||||||
private LocalDateTime startDate;
|
|
||||||
private short chapterPerDay;
|
|
||||||
private String creatorUserName;
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
package de.w665.biblenotes.rest;
|
package de.w665.biblenotes.rest.mappings;
|
||||||
|
|
||||||
import de.w665.biblenotes.rest.dto.AuthenticationRequest;
|
import de.w665.biblenotes.rest.ro.AuthenticationRequest;
|
||||||
import de.w665.biblenotes.service.AuthenticationService;
|
import de.w665.biblenotes.service.AuthenticationService;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -16,8 +16,9 @@ import java.util.Map;
|
|||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/auth")
|
@RequestMapping("/api/v1/auth")
|
||||||
public class AuthenticationController {
|
public class AuthenticationController {
|
||||||
|
|
||||||
private final AuthenticationService authenticationService;
|
private final AuthenticationService authenticationService;
|
||||||
|
|
||||||
public AuthenticationController(AuthenticationService authenticationService) {
|
public AuthenticationController(AuthenticationService authenticationService) {
|
||||||
@ -30,15 +31,9 @@ public class AuthenticationController {
|
|||||||
String token = authenticationService.authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword(), request.getRemoteAddr());
|
String token = authenticationService.authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword(), request.getRemoteAddr());
|
||||||
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
|
||||||
if(token == null) {
|
|
||||||
log.debug("Authentication failed for username: " + authenticationRequest.getUsername());
|
|
||||||
response.put("error", "Authentication failed. Username or password incorrect.");
|
|
||||||
return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
response.put("token", token);
|
response.put("token", token);
|
||||||
|
response.put("success", token != null);
|
||||||
|
|
||||||
if(token == null) {
|
if(token == null) {
|
||||||
log.debug("Authentication failed for username: " + authenticationRequest.getUsername());
|
log.debug("Authentication failed for username: " + authenticationRequest.getUsername());
|
||||||
return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED);
|
return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED);
|
||||||
@ -46,4 +41,4 @@ public class AuthenticationController {
|
|||||||
|
|
||||||
return new ResponseEntity<>(response, HttpStatus.OK);
|
return new ResponseEntity<>(response, HttpStatus.OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,73 +0,0 @@
|
|||||||
package de.w665.biblenotes.rest.mappings;
|
|
||||||
|
|
||||||
import de.w665.biblenotes.db.entity.BibleReadingPlan;
|
|
||||||
import de.w665.biblenotes.db.entity.User;
|
|
||||||
import de.w665.biblenotes.db.repo.BibleReadingPlanRepository;
|
|
||||||
import de.w665.biblenotes.db.repo.UserRepository;
|
|
||||||
import de.w665.biblenotes.rest.SecureApiRestController;
|
|
||||||
import de.w665.biblenotes.rest.dto.BibleReadingPlanDTO;
|
|
||||||
import de.w665.biblenotes.rest.security.UserAuthenticationContext;
|
|
||||||
import jakarta.persistence.EntityManager;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/secure/bible-reading-plan")
|
|
||||||
public class BibleReadingPlanMapping {
|
|
||||||
|
|
||||||
private final EntityManager entityManager;
|
|
||||||
private final BibleReadingPlanRepository bibleReadingPlanRepository;
|
|
||||||
|
|
||||||
public BibleReadingPlanMapping(UserRepository userRepository, EntityManager entityManager, BibleReadingPlanRepository bibleReadingPlanRepository) {
|
|
||||||
this.entityManager = entityManager;
|
|
||||||
this.bibleReadingPlanRepository = bibleReadingPlanRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
public ResponseEntity<Object> getBibleReadingPlans(@RequestParam(name = "id", required = true) Long id) {
|
|
||||||
Optional<BibleReadingPlan> brp = bibleReadingPlanRepository.findById(id);
|
|
||||||
if(brp.isEmpty()) {
|
|
||||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
BibleReadingPlanDTO bibleReadingPlanDTO = new BibleReadingPlanDTO();
|
|
||||||
bibleReadingPlanDTO.setId(brp.get().getId());
|
|
||||||
bibleReadingPlanDTO.setPlanName(brp.get().getPlanName());
|
|
||||||
bibleReadingPlanDTO.setChapterPerDay(brp.get().getChapterPerDay());
|
|
||||||
bibleReadingPlanDTO.setStartDate(brp.get().getStartDate());
|
|
||||||
bibleReadingPlanDTO.setCreatorUserName(brp.get().getCreatorUser().getUsername());
|
|
||||||
|
|
||||||
return new ResponseEntity<>(bibleReadingPlanDTO, HttpStatus.OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
public ResponseEntity<Object> createBibleReadingPlan(@RequestBody BibleReadingPlanDTO bibleReadingPlanDTO) {
|
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
|
|
||||||
// Construct entity
|
|
||||||
BibleReadingPlan plan = new BibleReadingPlan();
|
|
||||||
plan.setPlanName(bibleReadingPlanDTO.getPlanName());
|
|
||||||
plan.setChapterPerDay(bibleReadingPlanDTO.getChapterPerDay());
|
|
||||||
plan.setStartDate(bibleReadingPlanDTO.getStartDate());
|
|
||||||
|
|
||||||
// Set user as creator in db entity
|
|
||||||
User userProxy = entityManager.getReference(User.class, ((UserAuthenticationContext) auth.getPrincipal()).getUserId());
|
|
||||||
plan.setCreatorUser(userProxy);
|
|
||||||
BibleReadingPlan createdPlan = bibleReadingPlanRepository.save(plan);
|
|
||||||
|
|
||||||
// Construct response (we do this because otherwise the user object would be serialized (which would fail because it's a proxy + i addition we don't want to send the users credentials))
|
|
||||||
BibleReadingPlanDTO responseDTO = new BibleReadingPlanDTO();
|
|
||||||
responseDTO.setPlanName(createdPlan.getPlanName());
|
|
||||||
responseDTO.setChapterPerDay(createdPlan.getChapterPerDay());
|
|
||||||
responseDTO.setStartDate(createdPlan.getStartDate());
|
|
||||||
responseDTO.setCreatorUserName(((UserAuthenticationContext) auth.getPrincipal()).getUsername());
|
|
||||||
responseDTO.setId(createdPlan.getId());
|
|
||||||
|
|
||||||
return new ResponseEntity<>(responseDTO, HttpStatus.OK);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +1,14 @@
|
|||||||
package de.w665.biblenotes.rest.mappings;
|
package de.w665.biblenotes.rest.mappings;
|
||||||
|
|
||||||
import de.w665.biblenotes.rest.SecureApiRestController;
|
import de.w665.biblenotes.rest.ApiRestController;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class TestMapping extends SecureApiRestController {
|
public class TestController extends ApiRestController {
|
||||||
|
|
||||||
@GetMapping("/test")
|
@GetMapping("/test")
|
||||||
public String test() {
|
public String test() {
|
||||||
return "Your authentication works!";
|
return "Test";
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package de.w665.biblenotes.rest.dto;
|
package de.w665.biblenotes.rest.ro;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
@ -13,3 +13,4 @@ public class AuthenticationRequest {
|
|||||||
private String username;
|
private String username;
|
||||||
private String password;
|
private String password;
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
|||||||
package de.w665.biblenotes.rest.security;
|
package de.w665.biblenotes.rest.security;
|
||||||
|
|
||||||
import de.w665.biblenotes.db.entity.Role;
|
|
||||||
import de.w665.biblenotes.service.AuthenticationService;
|
import de.w665.biblenotes.service.AuthenticationService;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
@ -9,6 +8,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
@ -28,53 +28,46 @@ import java.util.List;
|
|||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final AuthenticationService authenticationService;
|
private final AuthenticationService authenticationService;
|
||||||
private final RequestMatcher requestMatcher = new AntPathRequestMatcher("/api/v1/secure/**");
|
private final RequestMatcher requestMatcher = new AntPathRequestMatcher("/api/v1/**"); // The filter will verify authentication for all requests starting with /api/v1/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
|
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
|
||||||
logger.debug("Filtering request: " + request.getRequestURI());
|
logger.debug("Filtering request: " + request.getRequestURI());
|
||||||
|
|
||||||
// Skip filter for all paths except the secure path
|
if ("/api/v1/auth/login".equals(request.getRequestURI())) {
|
||||||
|
logger.debug("Login request detected. Skipping JWT authentication.");
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(!requestMatcher.matches(request)) {
|
if(!requestMatcher.matches(request)) {
|
||||||
logger.debug("Request does not match the secure path. Skipping JWT authentication.");
|
logger.debug("Request does not match the secure path. Skipping JWT authentication.");
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the JWT token from the request
|
try {
|
||||||
String jwt = getJwtFromRequest(request);
|
String jwt = getJwtFromRequest(request);
|
||||||
|
if (jwt != null && authenticationService.validateToken(jwt)) {
|
||||||
|
String username = authenticationService.extractSubject(jwt);
|
||||||
|
// Extract the role from the JWT and set it to Spring AuthenticationContext for access control
|
||||||
|
String role = authenticationService.getClaimValue(jwt, "role", String.class);
|
||||||
|
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role));
|
||||||
|
|
||||||
if (jwt == null) { // Check if the JWT token is missing
|
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
response.setContentType("application/json");
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"JWT token is missing.\"}");
|
// SUCCESSFUL AUTHENTICATION
|
||||||
logger.warn("Unauthorized: JWT token is missing.");
|
filterChain.doFilter(request, response);
|
||||||
return;
|
} else {
|
||||||
|
logger.warn("Unauthorized: Authentication token is missing or invalid.");
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.warn("Could not set user authentication in security context. An error occurred during JWT processing.", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authenticationService.validateToken(jwt)) { // Validate the JWT token
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"JWT token is invalid.\"}");
|
|
||||||
logger.warn("Unauthorized: JWT token is invalid.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the role and username from the JWT and set it to Spring AuthenticationContext for access control
|
|
||||||
String username = authenticationService.getClaimValue(jwt, "username", String.class);
|
|
||||||
String role = authenticationService.getClaimValue(jwt, "role", String.class);
|
|
||||||
int userId = authenticationService.getClaimValue(jwt, "userId", Integer.class);
|
|
||||||
UserAuthenticationContext uac = new UserAuthenticationContext(username, userId, Role.valueOf(role));
|
|
||||||
|
|
||||||
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role));
|
|
||||||
|
|
||||||
JwtAuthenticationToken auth = new JwtAuthenticationToken(uac, jwt, authorities);
|
|
||||||
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
|
||||||
|
|
||||||
// SUCCESSFUL AUTHENTICATION
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getJwtFromRequest(HttpServletRequest request) {
|
private String getJwtFromRequest(HttpServletRequest request) {
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
package de.w665.biblenotes.rest.security;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
|
|
||||||
|
|
||||||
private final UserAuthenticationContext principal;
|
|
||||||
@Getter
|
|
||||||
private final String token;
|
|
||||||
|
|
||||||
public JwtAuthenticationToken(UserAuthenticationContext principal, String token, Collection<? extends GrantedAuthority> authorities) {
|
|
||||||
super(authorities);
|
|
||||||
this.principal = principal;
|
|
||||||
this.token = token;
|
|
||||||
super.setAuthenticated(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getCredentials() {
|
|
||||||
return "Credentials are not stored.";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public UserAuthenticationContext getPrincipal() {
|
|
||||||
return this.principal;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package de.w665.biblenotes.rest.security;
|
|
||||||
|
|
||||||
import de.w665.biblenotes.db.entity.Role;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class UserAuthenticationContext {
|
|
||||||
private String username;
|
|
||||||
private int userId;
|
|
||||||
private Role role;
|
|
||||||
}
|
|
@ -2,8 +2,8 @@ package de.w665.biblenotes.service;
|
|||||||
|
|
||||||
import de.w665.biblenotes.db.repo.UserLoginRepository;
|
import de.w665.biblenotes.db.repo.UserLoginRepository;
|
||||||
import de.w665.biblenotes.db.repo.UserRepository;
|
import de.w665.biblenotes.db.repo.UserRepository;
|
||||||
import de.w665.biblenotes.db.entity.User;
|
import de.w665.biblenotes.model.User;
|
||||||
import de.w665.biblenotes.db.entity.UserLogin;
|
import de.w665.biblenotes.model.UserLogin;
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import io.jsonwebtoken.Jwt;
|
import io.jsonwebtoken.Jwt;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
@ -15,7 +15,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -26,9 +25,9 @@ public class AuthenticationService {
|
|||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final UserLoginRepository userLoginRepository;
|
private final UserLoginRepository userLoginRepository;
|
||||||
|
|
||||||
@Value("${secureapi.jwt.secret}")
|
@Value("${jwt.secret}")
|
||||||
private String secretString;
|
private String secretString;
|
||||||
@Value("${secureapi.jwt.expiration}")
|
@Value("${jwt.expiration}")
|
||||||
private long expirationTime; // in milliseconds
|
private long expirationTime; // in milliseconds
|
||||||
private SecretKey secretKey;
|
private SecretKey secretKey;
|
||||||
|
|
||||||
@ -50,34 +49,27 @@ public class AuthenticationService {
|
|||||||
if(expirationTime.length > 0) {
|
if(expirationTime.length > 0) {
|
||||||
this.expirationTime = expirationTime[0];
|
this.expirationTime = expirationTime[0];
|
||||||
}
|
}
|
||||||
log.debug("Authenticating user: {}", username);
|
Optional<User> optionalUser = userRepository.retrieveUserByUsername(username);
|
||||||
Optional<User> optionalUser = userRepository.findByUsername(username);
|
|
||||||
if (optionalUser.isPresent() && passwordEncoder.matches(password, optionalUser.get().getPassword())) {
|
if (optionalUser.isPresent() && passwordEncoder.matches(password, optionalUser.get().getPassword())) {
|
||||||
User user = optionalUser.get();
|
User user = optionalUser.get();
|
||||||
|
|
||||||
UserLogin userLogin = new UserLogin();
|
userLoginRepository.insertUserLogin(new UserLogin(""/*Auto generated*/, user.getId(), new Date(), remoteAddr));
|
||||||
userLogin.setUserId(user.getId());
|
userRepository.updateLastLoginForUser(user.getUsername(), new Date());
|
||||||
userLogin.setLoginTime(LocalDateTime.now());
|
|
||||||
userLogin.setLoginIp(remoteAddr);
|
|
||||||
|
|
||||||
userLoginRepository.save(userLogin);
|
|
||||||
userRepository.updateLastLoginForUser(user.getUsername(), LocalDateTime.now());
|
|
||||||
return generateToken(user);
|
return generateToken(user);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateToken(User user) {
|
private String generateToken(User username) {
|
||||||
long nowMillis = System.currentTimeMillis();
|
long nowMillis = System.currentTimeMillis();
|
||||||
Date now = new Date(nowMillis);
|
Date now = new Date(nowMillis);
|
||||||
Date expiryDate = new Date(nowMillis + expirationTime);
|
Date expiryDate = new Date(nowMillis + expirationTime);
|
||||||
|
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
.subject("BibleNotes Authentication Token")
|
.subject("Biblenotes Authentication Token")
|
||||||
.issuedAt(now)
|
.issuedAt(now)
|
||||||
.claim("role", user.getRole())
|
.claim("role", username.getRole())
|
||||||
.claim("username", user.getUsername())
|
.claim("username", username.getUsername())
|
||||||
.claim("userId", user.getId())
|
|
||||||
.expiration(expiryDate)
|
.expiration(expiryDate)
|
||||||
.signWith(secretKey)
|
.signWith(secretKey)
|
||||||
.compact();
|
.compact();
|
||||||
|
@ -1,21 +1,19 @@
|
|||||||
spring.application.name=biblenotes
|
biblenotes.auto-reset-on-startup=false
|
||||||
server.port=665
|
biblenotes.management.user.username=admin
|
||||||
database.init.create-default-admin-user=true
|
biblenotes.management.user.password=admin
|
||||||
|
|
||||||
secureapi.jwt.secret=someSecretChangeMeInProduction
|
# Database
|
||||||
secureapi.jwt.expiration=86400000
|
rethinkdb.host=localhost
|
||||||
|
rethinkdb.port=28015
|
||||||
spring.datasource.url=jdbc:postgresql://localhost:5432/biblenotes
|
rethinkdb.database=biblenotes
|
||||||
spring.datasource.username=biblenotes
|
|
||||||
spring.datasource.password=ujhfdogiuhfdgiusdfhoviufdnpviusdhdfiuqbfoiudzsfzidfugofduszbdv
|
|
||||||
spring.jpa.hibernate.ddl-auto=update
|
|
||||||
spring.jpa.show-sql=true
|
|
||||||
spring.jpa.database=postgresql
|
|
||||||
|
|
||||||
|
# Logging
|
||||||
logging.level.de.w665.biblenotes=DEBUG
|
logging.level.de.w665.biblenotes=DEBUG
|
||||||
|
|
||||||
# Static path
|
# Static path
|
||||||
spring.web.resources.static-locations=classpath:/static/
|
spring.web.resources.static-locations=classpath:/static/browser/
|
||||||
|
|
||||||
# If this is removed, this prefix must be added to the security config
|
server.port=80
|
||||||
spring.mvc.servlet.path=/api/v1
|
spring.application.name=biblenotes
|
||||||
|
jwt.secret=sampleKeyToChangeInProduction
|
||||||
|
jwt.expiration=3600000
|
@ -1,86 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Log in</title>
|
|
||||||
<!-- Bootstrap CSS -->
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<!-- jQuery -->
|
|
||||||
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="container d-flex justify-content-center align-items-center min-vh-100">
|
|
||||||
<div class="card shadow p-4" style="width: 100%; max-width: 400px;">
|
|
||||||
<h3 class="text-center mb-4">Log In</h3>
|
|
||||||
<form id="loginForm">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="username" class="form-label">Username</label>
|
|
||||||
<input type="text" class="form-control" id="username" name="username" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">Password</label>
|
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<button type="submit" class="btn btn-primary w-100">Log In</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div class="mt-3 text-center" id="loader" style="display: none;">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 text-center" id="response" style="display: none;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
$(document).ready(function () {
|
|
||||||
$('#loginForm').on('submit', function (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// Hide previous messages and show loader
|
|
||||||
$('#response').hide().empty();
|
|
||||||
$('#loader').show();
|
|
||||||
|
|
||||||
// Get form data
|
|
||||||
const username = $('#username').val();
|
|
||||||
const password = $('#password').val();
|
|
||||||
|
|
||||||
// Send POST request
|
|
||||||
$.ajax({
|
|
||||||
url: 'http://localhost:665/api/v1/auth/login',
|
|
||||||
method: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify({ username, password }),
|
|
||||||
success: function (response) {
|
|
||||||
// Hide loader and display token
|
|
||||||
$('#loader').hide();
|
|
||||||
$('#response').html(`<div class="alert alert-success">Token: ${response.token}</div>`).show();
|
|
||||||
},
|
|
||||||
error: function () {
|
|
||||||
// Hide loader and show error message
|
|
||||||
$('#loader').hide();
|
|
||||||
$('#response').html('<div class="alert alert-danger">Failed to log in. Please try again.</div>').show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Allow pressing Enter to submit the form
|
|
||||||
$('#loginForm').on('keypress', function (event) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
$('#loginForm').submit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Bootstrap Bundle with Popper -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
x
Reference in New Issue
Block a user