Initial commit, working server
This commit is contained in:
@@ -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/
|
||||
+102
@@ -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> {
|
||||
}
|
||||
+7
@@ -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> {
|
||||
}
|
||||
+7
@@ -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);
|
||||
}
|
||||
}
|
||||
+26
@@ -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}
|
||||
Reference in New Issue
Block a user