Initial commit, working server

This commit is contained in:
Martin Asprusten
2026-04-12 01:28:25 +02:00
commit 49e857f258
39 changed files with 3375 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# Covers JetBrains IDEs: IntelliJ, GoLand, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
.idea/
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
.idea/sonarlint.xml # see https://community.sonarsource.com/t/is-the-file-idea-idea-idea-sonarlint-xml-intended-to-be-under-source-control/121119
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based HTTP Client
.idea/httpRequests
http-client.private.env.json
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# Apifox Helper cache
.idea/.cache/.Apifox_Helper
.idea/ApifoxUploaderProjectSetting.xml
# Github Copilot persisted session migrations, see: https://github.com/microsoft/copilot-intellij-feedback/issues/712#issuecomment-3322062215
.idea/**/copilot.data.migration.*.xml
target/
View File
View File
+102
View File
@@ -0,0 +1,102 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>no.asprusten</groupId>
<artifactId>Sykkelaksjon</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Sykkelaksjon</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>21</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>7.1.0.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>4.0.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
<version>4.0.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>4.0.5</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>spring-webmvc-pac4j</artifactId>
<version>8.0.2</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-oidc</artifactId>
<version>6.4.0</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-http</artifactId>
<version>6.4.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jsonschema2pojo</groupId>
<artifactId>jsonschema2pojo-maven-plugin</artifactId>
<version>1.3.3</version>
<configuration>
<sourceDirectory>${basedir}/../messages/</sourceDirectory>
<targetPackage>no.asprusten.sykkelaksjon.messages</targetPackage>
<useLongIntegers>true</useLongIntegers>
</configuration>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>4.0.5</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>no.asprusten.sykkelaksjon.Server</mainClass>
<layout>ZIP</layout>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,60 @@
package no.asprusten.sykkelaksjon;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertiesPropertySource;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
import java.util.Scanner;
public class PropertiesListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
// Make this actually find the postgres password and run when not in a Docker container
// Also make it find the static files
ConfigurableEnvironment environment = event.getEnvironment();
String passwordSetting = environment.getProperty("postgrespassword");
if (passwordSetting == null) {
File passwordFile = new File("../secrets/postgrespassword");
if (passwordFile.canRead()) {
try {
Scanner reader = new Scanner(passwordFile);
if (reader.hasNextLine()) {
String password = reader.nextLine();
Properties props = new Properties();
props.put("postgrespassword", password);
environment.getPropertySources().addFirst(new PropertiesPropertySource("passwordProps", props));
}
} catch (FileNotFoundException e) {
// Do nothing, continue
}
}
}
String staticResourcesPath = environment.getProperty("spring.web.resources.static-locations");
File staticResources = new File(staticResourcesPath.replaceFirst("file:", ""));
if (!staticResources.exists()) {
// Look for static resources in the default location
File nonDockerStatic = new File("../client/dist");
if (nonDockerStatic.exists()) {
Properties staticProps = new Properties();
try {
staticProps.put("spring.web.resources.static-locations", "file:" + nonDockerStatic.getCanonicalPath());
environment.getPropertySources().addFirst(new PropertiesPropertySource("staticProps", staticProps));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
@Override
public boolean supportsAsyncExecution() {
return ApplicationListener.super.supportsAsyncExecution();
}
}
@@ -0,0 +1,354 @@
package no.asprusten.sykkelaksjon;
import no.asprusten.sykkelaksjon.db.services.ActivityService;
import no.asprusten.sykkelaksjon.db.services.ActivityTemplateService;
import no.asprusten.sykkelaksjon.db.services.ActivityTypeService;
import no.asprusten.sykkelaksjon.db.services.UserService;
import no.asprusten.sykkelaksjon.messages.*;
import no.asprusten.sykkelaksjon.security.ServerExceptionHandler;
import org.pac4j.core.profile.ProfileManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* Hello world!
*/
@RestController
@SpringBootApplication
public class Server {
@Autowired
private ProfileManager profileManager;
@Autowired
private UserService userService;
@Autowired
private ActivityTypeService activityTypeService;
@Autowired
private ActivityService activityService;
@Autowired
private ActivityTemplateService activityTemplateService;
@Value("${sykkelaksjon.openid.discoveryURI}")
private String discoveryURI;
@Value("${sykkelaksjon.openid.clientId}")
private String clientId;
private ActivityType getActivityTypeMessage(no.asprusten.sykkelaksjon.db.datatypes.ActivityType activityType) {
ActivityType activityTypeMessage = new ActivityType();
activityTypeMessage.setId(activityType.getId());
activityTypeMessage.setName(activityType.getActivityType());
activityTypeMessage.setConversionFactor(activityType.getConversion());
activityTypeMessage.setUnit(activityType.getUnit());
return activityTypeMessage;
}
private ActivityTemplate getActivityTemplateMessage(no.asprusten.sykkelaksjon.db.datatypes.ActivityTemplate activityTemplate) {
ActivityTemplate templateMessage = new ActivityTemplate();
templateMessage.setId(activityTemplate.getId());
templateMessage.setActivityType(getActivityTypeMessage(activityTemplate.getActivityType()));
templateMessage.setName(activityTemplate.getName());
templateMessage.setNumberOfUnits(activityTemplate.getNumberOfUnits());
return templateMessage;
}
private Activity getActivityMessage(no.asprusten.sykkelaksjon.db.datatypes.Activity activity) {
Activity activityMessage = new Activity();
activityMessage.setId(activity.getId());
activityMessage.setActivityType(getActivityTypeMessage(activity.getActivityType()));
activityMessage.setDescription(activity.getDescription());
activityMessage.setNumberOfUnits(activity.getNumberOfUnits());
activityMessage.setDate(activity.getDate().format(DateTimeFormatter.ISO_LOCAL_DATE));
return activityMessage;
}
@CrossOrigin(allowCredentials = "true", origins = {"http://localhost:5173"})
@GetMapping("/api")
public ServerMessageSchema respondToRequest() throws ServerExceptionHandler.InvalidUserException {
var optionalUserProfile = profileManager.getProfile();
if (optionalUserProfile.isEmpty()) {
throw new ServerExceptionHandler.InvalidUserException();
}
var userProfile = optionalUserProfile.get();
var optionalUser = userService.getUser(userProfile.getUsername());
var user = optionalUser
.orElseGet(
() -> userService.createUser(
userProfile.getUsername(),
userProfile.getAttribute("name").toString()
)
);
var allUsers = userService.list();
ServerMessageSchema serverMessage = new ServerMessageSchema();
serverMessage.setName(user.getFullName());
serverMessage.setIsAdmin(user.isAdmin());
boolean unitConversionNecessary = false;
for (var activityType : activityTypeService.list()) {
serverMessage.getActivityTypes().add(getActivityTypeMessage(activityType));
unitConversionNecessary |= activityType.getConversion() != 1.0;
}
serverMessage.setUnitConversionNecessary(unitConversionNecessary);
for (var activityTemplate : user.getTemplates()) {
serverMessage.getActivityTemplates().add(getActivityTemplateMessage(activityTemplate));
}
List<Activity> unsortedActivities = new ArrayList<>();
for (var activity : user.getActivities()) {
unsortedActivities.add(getActivityMessage(activity));
}
unsortedActivities.sort(Comparator.comparing(Activity::getDate));
serverMessage.getActivities().addAll(unsortedActivities.reversed());
List<OtherUser> unsortedOtherUsers = new ArrayList<>();
for (var otherUser : allUsers) {
// Don't describe the current user
if (otherUser.getId().equals(user.getId())) {
continue;
}
// If this is not an active user, and the requesting user is not an admin, skip
if (!otherUser.isActive() && !user.isAdmin()) {
continue;
}
// If the user has not registered anything yet, also skip it (unless admin)
if (otherUser.getActivities().isEmpty() && !user.isAdmin()) {
continue;
}
OtherUser otherUserMessage = new OtherUser();
otherUserMessage.setName(otherUser.getFullName());
double totalKilometers = otherUser.getActivities()
.stream()
.mapToDouble(activity -> activity.getActivityType().getConversion() * activity.getNumberOfUnits())
.sum();
otherUserMessage.setTotalKilometers(totalKilometers);
otherUser.getActivities().stream()
.map(no.asprusten.sykkelaksjon.db.datatypes.Activity::getDate)
.min(LocalDate::compareTo)
.ifPresent(earliestDate ->
otherUserMessage.setEarliestActivity(earliestDate.format(DateTimeFormatter.ISO_LOCAL_DATE))
);
otherUser.getActivities().stream()
.map(no.asprusten.sykkelaksjon.db.datatypes.Activity::getDate)
.max(LocalDate::compareTo)
.ifPresent(latestDate ->
otherUserMessage.setLatestActivity(latestDate.format(DateTimeFormatter.ISO_LOCAL_DATE))
);
// Additional info is only for administrators
if (user.isAdmin()) {
otherUserMessage.setUserId(otherUser.getId());
otherUserMessage.setUserName(otherUser.getUsername());
otherUserMessage.setIsActive(otherUser.isActive());
otherUserMessage.setIsAdmin(otherUser.isAdmin());
List<Activity> userUnsortedActivities = new ArrayList<>();
for (var activity : otherUser.getActivities()) {
userUnsortedActivities.add(getActivityMessage(activity));
}
userUnsortedActivities.sort(Comparator.comparing(Activity::getDate));
otherUserMessage.getActivities().addAll(userUnsortedActivities.reversed());
for (var activityTemplate : otherUser.getTemplates()) {
otherUserMessage.getActivityTemplates().add(getActivityTemplateMessage(activityTemplate));
}
}
unsortedOtherUsers.add(otherUserMessage);
}
unsortedOtherUsers.sort(Comparator.comparing(OtherUser::getTotalKilometers));
serverMessage.getOtherUsers().addAll(unsortedOtherUsers.reversed());
return serverMessage;
}
@CrossOrigin(allowCredentials = "true", origins = {"http://localhost:5173"})
@PostMapping(path = "/api/submitActivity", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void submitActivity(
@RequestParam("activity-type") Long activityTypeId,
@RequestParam("activity-distance") double distance,
@RequestParam("activity-description") String description,
@RequestParam("activity-date") String date
) {
profileManager.getProfile().ifPresent(userProfile -> {
String username = userProfile.getUsername();
userService.getUser(username).ifPresent(dbUser -> {
activityTypeService.getById(activityTypeId).ifPresent(activityType -> {
no.asprusten.sykkelaksjon.db.datatypes.Activity activity = new no.asprusten.sykkelaksjon.db.datatypes.Activity(
activityType,
dbUser,
distance,
description,
LocalDate.parse(date)
);
activityService.saveActivity(activity);
});
});
});
}
@CrossOrigin(allowCredentials = "true", origins = {"http://localhost:5173"})
@PostMapping(path = "/api/submitActivityTemplate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void submitActivityTemplate(
@RequestParam("activity-type") Long activityTypeId,
@RequestParam("activity-distance") double distance,
@RequestParam("activity-description") String description,
@RequestParam("activity-date") String date
) {
profileManager.getProfile().ifPresent(userProfile -> {
String username = userProfile.getUsername();
userService.getUser(username).ifPresent(dbUser -> {
activityTypeService.getById(activityTypeId).ifPresent(activityType -> {
no.asprusten.sykkelaksjon.db.datatypes.ActivityTemplate activityTemplate = new no.asprusten.sykkelaksjon.db.datatypes.ActivityTemplate(
dbUser,
activityType,
description,
distance
);
activityTemplateService.saveActivityTemplate(activityTemplate);
});
});
});
}
@CrossOrigin(allowCredentials = "true", origins = {"http://localhost:5173"})
@DeleteMapping(path = "/api/deleteActivityTemplate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void deleteActivityTemplate(@RequestParam("activity-template-id") Long activityTemplateId) {
profileManager.getProfile().ifPresent(userProfile -> {
String username = userProfile.getUsername();
userService.getUser(username).ifPresent(dbUser -> {
activityTemplateService.findById(activityTemplateId).ifPresent(activityTemplate -> {
if (activityTemplate.getOwner().equals(dbUser)) {
activityTemplateService.deleteActivityTemplate(activityTemplate.getId());
}
});
});
});
}
@CrossOrigin(allowCredentials = "true", origins = {"http://localhost:5173"})
@DeleteMapping(path = "/api/deleteActivity", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void deleteActivity(@RequestParam("activity-id") Long activityId) {
profileManager.getProfile().ifPresent(userProfile -> {
String username = userProfile.getUsername();
userService.getUser(username).ifPresent(dbUser -> {
activityService.findById(activityId).ifPresent(activity -> {
if (activity.getActivityOwner().equals(dbUser) || dbUser.isAdmin()) {
activityService.deleteActivity(activityId);
}
});
});
});
}
@CrossOrigin(allowCredentials = "true", origins = {"http://localhost:5173"})
@PostMapping(path = "/api/addActivityType", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void createActivityType(
@RequestParam("activity-type-name") String name,
@RequestParam("activity-type-unit") String unit,
@RequestParam("activity-type-conversion-factor") double conversionFactor
) {
profileManager.getProfile().ifPresent(userProfile -> {
String username = userProfile.getUsername();
userService.getUser(username).ifPresent(dbUser -> {
if (dbUser.isAdmin()) {
no.asprusten.sykkelaksjon.db.datatypes.ActivityType activityType = new no.asprusten.sykkelaksjon.db.datatypes.ActivityType(
name, unit, conversionFactor
);
activityTypeService.saveActivityType(activityType);
}
});
});
}
@CrossOrigin(allowCredentials = "true", origins = {"http://localhost:5173"})
@DeleteMapping(path = "/api/deleteActivityType", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void deleteActivityType(@RequestParam("activity-type-id") Long activityTypeId) {
profileManager.getProfile().ifPresent(userProfile -> {
String username = userProfile.getUsername();
userService.getUser(username).ifPresent(dbUser -> {
if (dbUser.isAdmin()) {
activityTypeService.deleteActivityType(activityTypeId);
}
});
});
}
@CrossOrigin(allowCredentials = "true", origins = {"http://localhost:5173"})
@PutMapping(path = "/api/makeAdmin", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void makeAdmin(@RequestParam("user-id") Long userId) {
profileManager.getProfile().ifPresent(userProfile -> {
String username = userProfile.getUsername();
userService.getUser(username).ifPresent(dbUser -> {
if (dbUser.isAdmin()) {
userService.getUserById(userId).ifPresent(elevatedUser -> {
elevatedUser.setAdmin(true);
userService.saveUser(elevatedUser);
});
}
});
});
}
@CrossOrigin(allowCredentials = "true", origins = {"http://localhost:5173"})
@PutMapping(path = "/api/removeAdmin", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void removeAdmin(@RequestParam("user-id") Long userId) {
profileManager.getProfile().ifPresent(userProfile -> {
String username = userProfile.getUsername();
userService.getUser(username).ifPresent(dbUser -> {
if (dbUser.isAdmin()) {
userService.getUserById(userId).ifPresent(elevatedUser -> {
elevatedUser.setAdmin(false);
userService.saveUser(elevatedUser);
});
}
});
});
}
@CrossOrigin(allowCredentials = "true", origins = {"http://localhost:5173"})
@DeleteMapping(path = "/api/deleteUser", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void deleteUser(@RequestParam("user-id") Long userId) {
profileManager.getProfile().ifPresent(userProfile -> {
String username = userProfile.getUsername();
userService.getUser(username).ifPresent(dbUser -> {
if (dbUser.isAdmin()) {
userService.deleteUserById(userId);
}
});
});
}
@CrossOrigin(allowCredentials = "true", origins = {"http://localhost:5173"})
@GetMapping(path = "/api/openid")
public OpenidSchema provideOpenidConfig() {
OpenidSchema openidSchema = new OpenidSchema();
openidSchema.setClientId(clientId);
openidSchema.setOpenidDiscoveryUri(discoveryURI);
return openidSchema;
}
public static void main(String[] args) {
SpringApplication application = new SpringApplication(Server.class);
application.addListeners(new PropertiesListener());
application.run(args);
}
}
@@ -0,0 +1,67 @@
package no.asprusten.sykkelaksjon.db.datatypes;
import jakarta.persistence.*;
import java.time.LocalDate;
@Entity
public class Activity {
@Id
@GeneratedValue
private long id;
@ManyToOne( optional = false)
private ActivityType activityType;
@ManyToOne(optional = false)
private WebUser activityOwner;
@Column(nullable = false)
private Double numberOfUnits;
@Column(nullable = false)
private String description;
@Column(nullable = false)
private LocalDate date;
public Activity() {
}
public Activity(ActivityType activityType, WebUser activityOwner, Double numberOfUnits, String description, LocalDate date) {
this.activityType = activityType;
this.activityOwner = activityOwner;
this.numberOfUnits = numberOfUnits;
this.description = description;
this.date = date;
}
public long getId() {
return id;
}
public ActivityType getActivityType() {
return activityType;
}
public void setActivityType(ActivityType activityType) {
this.activityType = activityType;
}
public Double getNumberOfUnits() {
return numberOfUnits;
}
public void setNumberOfUnits(Double numberOfUnits) {
this.numberOfUnits = numberOfUnits;
}
public WebUser getActivityOwner() {
return activityOwner;
}
public String getDescription() {
return description;
}
public LocalDate getDate() {
return date;
}
}
@@ -0,0 +1,70 @@
package no.asprusten.sykkelaksjon.db.datatypes;
import jakarta.persistence.*;
@Entity
@Table(uniqueConstraints = {
@UniqueConstraint(name = "UniqueNamesPerUser", columnNames = { "owner", "name" })
})
public class ActivityTemplate {
@Id
@GeneratedValue
private long id;
@ManyToOne(optional = false)
@JoinColumn(name = "owner", nullable = false)
private WebUser owner;
@ManyToOne(optional = false)
private ActivityType activityType;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Double numberOfUnits;
public ActivityTemplate() {
}
public ActivityTemplate(WebUser owner, ActivityType activityType, String name, Double numberOfUnits) {
this.owner = owner;
this.activityType = activityType;
this.name = name;
this.numberOfUnits = numberOfUnits;
}
public Long getId() {
return id;
}
public WebUser getOwner() {
return owner;
}
public void setOwner(WebUser owner) {
this.owner = owner;
}
public ActivityType getActivityType() {
return activityType;
}
public void setActivityType(ActivityType activityType) {
this.activityType = activityType;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getNumberOfUnits() {
return numberOfUnits;
}
public void setNumberOfUnits(Double numberOfUnits) {
this.numberOfUnits = numberOfUnits;
}
}
@@ -0,0 +1,58 @@
package no.asprusten.sykkelaksjon.db.datatypes;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class ActivityType {
@Id
@GeneratedValue
private long id;
@Column(nullable = false, unique = true)
private String activityType;
@Column(nullable = false)
private String unit;
@Column(nullable = false)
private Double conversion;
public ActivityType() {
}
public ActivityType(String activityType, String unit, Double conversion) {
this.activityType = activityType;
this.unit = unit;
this.conversion = conversion;
}
public long getId() {
return id;
}
public String getActivityType() {
return activityType;
}
public String getUnit() {
return unit;
}
public Double getConversion() {
return conversion;
}
public void setActivityType(String activityType) {
this.activityType = activityType;
}
public void setUnit(String unit) {
this.unit = unit;
}
public void setConversion(Double conversion) {
this.conversion = conversion;
}
}
@@ -0,0 +1,88 @@
package no.asprusten.sykkelaksjon.db.datatypes;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(uniqueConstraints = {
@UniqueConstraint(name = "OnlyOneActiveWithUsername", columnNames = { "username", "zeroIfActive" })
})
public class WebUser {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String fullName;
private boolean isAdmin;
@Column(nullable = false)
private Long zeroIfActive;
@OneToMany(mappedBy = "activityOwner", fetch = FetchType.EAGER)
private List<Activity> activities = new ArrayList<>();
@OneToMany(mappedBy = "owner", fetch = FetchType.EAGER)
private List<ActivityTemplate> templates = new ArrayList<>();
public WebUser() {
}
public WebUser(String username, String fullName, boolean isAdmin) {
this.username = username;
this.fullName = fullName;
this.isAdmin = isAdmin;
this.zeroIfActive = 0L;
}
public Long getId() {
return id;
}
public String getUsername() {
return username;
}
public String getFullName() {
return fullName;
}
public boolean isAdmin() {
return isAdmin;
}
public boolean isActive() {
return zeroIfActive == 0L;
}
public void setUsername(String username) {
this.username = username;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public void setAdmin(boolean admin) {
isAdmin = admin;
}
public void setActive(boolean active) {
if (active) {
zeroIfActive = 0L;
} else {
zeroIfActive = id;
}
}
public List<Activity> getActivities() {
return activities;
}
public List<ActivityTemplate> getTemplates() {
return templates;
}
}
@@ -0,0 +1,7 @@
package no.asprusten.sykkelaksjon.db.repositories;
import no.asprusten.sykkelaksjon.db.datatypes.Activity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ActivityRepository extends JpaRepository<Activity, Long> {
}
@@ -0,0 +1,7 @@
package no.asprusten.sykkelaksjon.db.repositories;
import no.asprusten.sykkelaksjon.db.datatypes.ActivityTemplate;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ActivityTemplateRepository extends JpaRepository<ActivityTemplate, Long> {
}
@@ -0,0 +1,7 @@
package no.asprusten.sykkelaksjon.db.repositories;
import no.asprusten.sykkelaksjon.db.datatypes.ActivityType;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ActivityTypeRepository extends JpaRepository<ActivityType, Long> {
}
@@ -0,0 +1,12 @@
package no.asprusten.sykkelaksjon.db.repositories;
import no.asprusten.sykkelaksjon.db.datatypes.WebUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface WebUserRepository extends JpaRepository<WebUser, Long> {
List<WebUser> findByUsername(String username);
}
@@ -0,0 +1,26 @@
package no.asprusten.sykkelaksjon.db.services;
import no.asprusten.sykkelaksjon.db.datatypes.Activity;
import no.asprusten.sykkelaksjon.db.repositories.ActivityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class ActivityService {
@Autowired
ActivityRepository activityRepository;
public void saveActivity(Activity activity) {
activityRepository.save(activity);
}
public void deleteActivity(Long id) {
activityRepository.deleteById(id);
}
public Optional<Activity> findById(long id) {
return activityRepository.findById(id);
}
}
@@ -0,0 +1,26 @@
package no.asprusten.sykkelaksjon.db.services;
import no.asprusten.sykkelaksjon.db.datatypes.ActivityTemplate;
import no.asprusten.sykkelaksjon.db.repositories.ActivityTemplateRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class ActivityTemplateService {
@Autowired
private ActivityTemplateRepository activityTemplateRepository;
public Optional<ActivityTemplate> findById(Long id) {
return activityTemplateRepository.findById(id);
}
public void saveActivityTemplate(ActivityTemplate template) {
activityTemplateRepository.save(template);
}
public void deleteActivityTemplate(Long id) {
activityTemplateRepository.deleteById(id);
}
}
@@ -0,0 +1,31 @@
package no.asprusten.sykkelaksjon.db.services;
import no.asprusten.sykkelaksjon.db.datatypes.ActivityType;
import no.asprusten.sykkelaksjon.db.repositories.ActivityTypeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class ActivityTypeService {
@Autowired
private ActivityTypeRepository activityTypeRepository;
public List<ActivityType> list() {
return activityTypeRepository.findAll();
}
public Optional<ActivityType> getById(Long id) {
return activityTypeRepository.findById(id);
}
public ActivityType saveActivityType(ActivityType activityType) {
return activityTypeRepository.save(activityType);
}
public void deleteActivityType(Long id) {
activityTypeRepository.deleteById(id);
}
}
@@ -0,0 +1,59 @@
package no.asprusten.sykkelaksjon.db.services;
import jakarta.persistence.EntityManager;
import no.asprusten.sykkelaksjon.db.datatypes.WebUser;
import no.asprusten.sykkelaksjon.db.repositories.WebUserRepository;
import org.apache.catalina.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class UserService {
@Autowired
private WebUserRepository webUserRepository;
@Autowired
@Value("${sykkelaksjon.initial-admin}")
private String initialAdmin;
public List<WebUser> list() {
return webUserRepository.findAll();
}
public long getUserCount() {
return webUserRepository.count();
}
public Optional<WebUser> getUser(String username) {
List<WebUser> users = webUserRepository.findByUsername(username);
for (var user : users) {
if (user.isActive()) {
return Optional.of(user);
}
}
return Optional.empty();
}
public Optional<WebUser> getUserById(Long id) {
return webUserRepository.findById(id);
}
public WebUser createUser(String username, String fullname) {
boolean isAdmin = username.equals(initialAdmin);
WebUser newUser = new WebUser(username, fullname, isAdmin);
newUser = webUserRepository.save(newUser);
return newUser;
}
public WebUser saveUser(WebUser user) {
return webUserRepository.save(user);
}
public void deleteUserById(Long id) {
webUserRepository.deleteById(id);
}
}
@@ -0,0 +1,45 @@
package no.asprusten.sykkelaksjon.security;
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
import org.pac4j.core.authorization.authorizer.DefaultAuthorizers;
import org.pac4j.core.config.Config;
import org.pac4j.http.client.direct.HeaderClient;
import org.pac4j.oidc.client.OidcClient;
import org.pac4j.oidc.config.OidcConfiguration;
import org.pac4j.springframework.config.Pac4jSecurityConfig;
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.web.servlet.config.annotation.InterceptorRegistry;
import java.util.List;
@Configuration
public class SecurityConfig extends Pac4jSecurityConfig {
@Value("${sykkelaksjon.openid.discoveryURI}")
private String discoveryURI;
@Value("${sykkelaksjon.openid.clientId}")
private String clientId;
@Bean
public Config config() {
final var config = new OidcConfiguration()
.setDiscoveryURI(discoveryURI)
.setClientId(clientId);
OidcClient client = new OidcClient(config);
client.setCallbackUrl("notused");
client.init();
HeaderClient headerClient = new HeaderClient("Authorization", "Bearer ", client.getProfileCreator());
return new Config(headerClient);
}
@Override
public void addInterceptors(final InterceptorRegistry registry) {
addSecurity(registry, "HeaderClient").addPathPatterns("/api/**").excludePathPatterns("/api/openid").excludeHttpMethods(List.of(HttpMethod.OPTIONS));
}
}
@@ -0,0 +1,17 @@
package no.asprusten.sykkelaksjon.security;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
public class ServerExceptionHandler {
public static class InvalidUserException extends Exception {}
@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(InvalidUserException.class)
public void handleInvalidUser() {
// Do nothing
}
}
@@ -0,0 +1,11 @@
spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:9876/sykkelaksjon}
spring.config.import=optional:configtree:/run/secrets/
spring.datasource.username=sykkelaksjon
spring.datasource.password=${postgrespassword}
spring.jpa.hibernate.ddl-auto=update
sykkelaksjon.initial-admin=${SYKKELAKSJON_INITIAL_ADMIN:martin}
spring.web.resources.static-locations=./static
sykkelaksjon.openid.discoveryURI = ${OPENID_DISCOVERY_URI}
sykkelaksjon.openid.clientId = ${OPENID_CLIENT_ID}