Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,27 @@ DB_NAME=codestardb

# Spring Boot / JWT
JWT_SECRET=change_me_use_a_long_random_string_at_least_64_chars
JWT_EXPIRATION=86400000
JWT_EXPIRATION=604800 # one week by default (in seconds)

# Signup open (true = No invitation code needed)
SIGNUP_OPEN=true

# Bootstrap super-admin on first boot.
# If the email exists in the database with a lower role, it is promoted. The password NEVER overwrites an existing user.
CODESTAR_BOOTSTRAP_SUPER_ADMIN_EMAIL=example@gmail.com
CODESTAR_BOOTSTRAP_SUPER_ADMIN_PASSWORD=change_me_to_a_strong_password
CODESTAR_BOOTSTRAP_SUPER_ADMIN_DISPLAY_NAME=John

# Ports (optional — defaults shown)
BACKEND_PORT=8080
FRONTEND_PORT=3000

# Next.js — URL the browser uses to reach the backend API
NEXT_PUBLIC_API_URL=http://localhost:8080
# Path to an external branding JSON file (optional) - Leave empty to use the bundled default
# You can customize the branding either by pointing to an external file OR by editing the bundled one.
INSTANCE_CONFIG_PATH=

# Frontend
# Name of the httpOnly cookie storing the JWT (must match backend + frontend code expectations).
AUTH_COOKIE_NAME=codestar_token
# Public canonical origin (metadata, robots.txt, sitemap.xml).
SITE_URL=http://localhost:3000
73 changes: 70 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,80 @@
# Environment secrets
# Environment / secrets
.env
.env.*
!.env.example
*.pem
*.key
*.crt

# OS
.DS_Store
.AppleDouble
.LSOverride
._*
.Spotlight-V100
.Trashes
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
Desktop.ini
$RECYCLE.BIN/
*.lnk
.fuse_hidden*
.directory
.Trash-*
.nfs*

# IDE
# IDE / Editors
.idea/
*.iml
*.iws
*.ipr
out/

# Claude
# VS Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
.history/

# Eclipse
.classpath
.project
.settings/
.metadata/
.factorypath
.apt_generated/

# NetBeans
nbproject/private/
nbbuild/
nbdist/
.nb-gradle/

# Vim / Emacs / Sublime / Misc
*.swp
*.swo
*~
.*.sw?
*.sublime-workspace
*.sublime-project
.\#*
\#*\#

# AI assistants / agents
.claude/
.aider*
.cursor/

# Logs / temp
*.log
logs/
tmp/
temp/

# have to be delete
codestar-draft-design/
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"i18n-ally.localesPaths": [
"apps/frontend/messages"
],
"java.configuration.updateBuildConfiguration": "interactive"
}
41 changes: 41 additions & 0 deletions apps/backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Maven
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
dependency-reduced-pom.xml
.flattened-pom.xml
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
buildNumber.properties

# Gradle (safety net)
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

# Java
*.class
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
*.ctxt
.mtj.tmp/
hs_err_pid*
replay_pid*

# Spring Boot
HELP.md
.springBeans

# Logs
*.log
4 changes: 2 additions & 2 deletions apps/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Stage 1: Build
# Build
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app

Expand All @@ -9,7 +9,7 @@ RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B

# Stage 2: Runtime
# Runtime
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app

Expand Down
17 changes: 16 additions & 1 deletion apps/backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<artifactId>backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backend</name>
<description>Demo project for Spring Boot</description>
<description>API backend for CodeStar</description>

<properties>
<java.version>17</java.version>
Expand Down Expand Up @@ -42,12 +42,27 @@
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>

<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,83 @@
package com.codestar.backend.config;

import com.codestar.backend.security.ApiSecurityExceptionHandler;
import com.codestar.backend.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

private final JwtAuthenticationFilter jwtFilter;
private final ApiSecurityExceptionHandler securityExceptionHandler;

@Value("${codestar.cors.allowed-origins}")
private List<String> allowedOrigins;

public SecurityConfig(JwtAuthenticationFilter jwtFilter, ApiSecurityExceptionHandler securityExceptionHandler) {
this.jwtFilter = jwtFilter;
this.securityExceptionHandler = securityExceptionHandler;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
http
.cors(cors -> cors.configurationSource(corsSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// public auth + branding
.requestMatchers(HttpMethod.POST,
"/api/v1/auth/login",
"/api/v1/auth/register").permitAll()
.requestMatchers(HttpMethod.GET,
"/api/v1/instance/branding").permitAll()
// swager/openAPI
.requestMatchers(
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html").permitAll()
// TODO .requestMatchers("/api/v1/courses/**").permitAll()
.anyRequest().authenticated())
.exceptionHandling(ex -> ex
.authenticationEntryPoint(securityExceptionHandler)
.accessDeniedHandler(securityExceptionHandler))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

@Bean
public CorsConfigurationSource corsSource() {
CorsConfiguration cfg = new CorsConfiguration();
cfg.setAllowedOrigins(allowedOrigins);
cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
cfg.setAllowedHeaders(List.of("*"));
cfg.setAllowCredentials(false);
UrlBasedCorsConfigurationSource src = new UrlBasedCorsConfigurationSource();
src.registerCorsConfiguration("/**", cfg);
return src;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.codestar.backend.config;

import com.codestar.backend.model.Role;
import com.codestar.backend.model.User;
import com.codestar.backend.repository.IUserRepository;
import com.codestar.backend.utils.Emails;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.Optional;

/**
* Create or promotes a super-admin on boot or desactivate if both variables aren't set
*/
@Configuration
public class SuperAdminBootstrap {

private static final Logger log = LoggerFactory.getLogger(SuperAdminBootstrap.class);

@Value("${codestar.bootstrap.super-admin.email:}")
private String bootstrapEmail;

@Value("${codestar.bootstrap.super-admin.password:}")
private String bootstrapPassword;

@Value("${codestar.bootstrap.super-admin.display-name:Super-admin}")
private String bootstrapDisplayName;

@Bean
public CommandLineRunner superAdminRunner(IUserRepository userRepository,
PasswordEncoder passwordEncoder) {
return args -> {
if (bootstrapEmail == null || bootstrapEmail.isBlank()) {
return;
}
if (bootstrapPassword == null || bootstrapPassword.isBlank()) {
log.warn("Bootstrap : super-admin email set but password missing — skipping.");
return;
}

String normalizedEmail = Emails.normalize(bootstrapEmail);
Optional<User> existing = userRepository.findByEmail(normalizedEmail);

if (existing.isEmpty()) {
User u = new User(
normalizedEmail,
passwordEncoder.encode(bootstrapPassword),
bootstrapDisplayName,
Role.SUPER_ADMIN);
userRepository.save(u);
log.info("Bootstrap : super-admin created");
return;
}

User user = existing.get();
if (user.getRole() != Role.SUPER_ADMIN) {
log.info("Bootstrap : user promoted from {} role to super-admin role", user.getRole());
user.setRole(Role.SUPER_ADMIN);
userRepository.save(user);
}
};
}
}
Loading
Loading