Merge commit '6bdd5c63ea0818122889d74841d8299a30fd4b40' into HEAD

This commit is contained in:
Jenkins 2025-01-10 13:44:08 +01:00
commit 6d754fee91
24 changed files with 410 additions and 51 deletions

@ -0,0 +1,14 @@
name: Remove branch deployment from branch.dsv.su.se
on:
pull_request:
types:
- closed
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- uses: https://gitea.dsv.su.se/ansv7779/action-branch-deploy@v2
with:
cleanup-ssh-key: ${{ secrets.BRANCH_CLEANUP_KEY }}
compose-file: compose-branch-deploy.yaml
mode: 'cleanup'

@ -0,0 +1,26 @@
name: Deploy to branch.dsv.su.se
on:
- pull_request
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- id: deploy
uses: https://gitea.dsv.su.se/ansv7779/action-branch-deploy@v2
with:
ssh-key: ${{ secrets.BRANCH_DEPLOY_KEY }}
compose-file: compose-branch-deploy.yaml
- name: Post URL to deployment as comment
uses: actions/github-script@v7
if: github.event.action == 'opened'
env:
BRANCH_URL: ${{ steps.deploy.outputs.url }}
with:
script: |
const url = process.env.BRANCH_URL;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Deployed to ${url}`
})

39
Dockerfile Normal file

@ -0,0 +1,39 @@
FROM debian:bookworm AS build
RUN apt-get update && apt-get install -y openjdk-17-jdk-headless
WORKDIR /app
COPY pom.xml .
COPY .mvn/ .mvn/
COPY mvnw .
COPY api/pom.xml api/pom.xml
COPY core/pom.xml core/pom.xml
COPY view/pom.xml view/pom.xml
COPY war/pom.xml war/pom.xml
COPY daisy-integration/pom.xml daisy-integration/pom.xml
# Download dependencies in a separate layer to allow caching for future builds
RUN ./mvnw dependency:go-offline \
--batch-mode \
--define includeScope=compile \
--activate-profiles docker-dependencies
COPY api/src/ api/src/
COPY core/src/ core/src/
COPY view/src/ view/src/
COPY war/src/ war/src/
COPY daisy-integration/src/ daisy-integration/src/
RUN ./mvnw package \
--offline \
--define skipTests \
--activate-profiles branch,DEV \
--define skip.npm \
--define skip.installnodenpm
FROM tomcat:10 AS run
COPY --from=build /app/war/target/*.war /usr/local/tomcat/webapps/ROOT.war
EXPOSE 8080

@ -39,3 +39,13 @@ can be performed.
Go to `Settings -> Language & Frameworks -> JavaScript -> Prettier` and then check
`Automatic Prettier Configuration`, set `Run for files` to `**/*.{java}`,
and finally check `Run on save`.
## Test servers
All pull requests are automatically deployed to a test server.
The URL to the test server will be posted as a comment in the pull request once deployed.
Prepare test data in the `DataInitializer` class to help others test your changes.
Document (in the pull request) which users to log in as and what to do to see the changes.
If you want to reset the data to its original state you can re-run the "deploy-branch.yaml"
workflow at https://gitea.dsv.su.se/DMC/scipro/actions for the branch you want to reset.

@ -0,0 +1,72 @@
services:
scipro:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
db:
condition: service_healthy
oauth2:
condition: service_started
environment:
- JDBC_DATABASE_URL=jdbc:mariadb://db:3306/scipro
- JDBC_DATABASE_USERNAME=scipro
- JDBC_DATABASE_PASSWORD=scipro
- OAUTH2_AUTHORIZATION_URI=https://oauth2-${VHOST}/authorize
- OAUTH2_TOKEN_URI=https://oauth2-${VHOST}/exchange
- OAUTH2_USER_INFO_URI=https://oauth2-${VHOST}/verify
- OAUTH2_CLIENT_ID=scipro_client
- OAUTH2_CLIENT_SECRET=scipro_secret
- OAUTH2_RESOURCE_SERVER_ID=scipro_api_client
- OAUTH2_RESOURCE_SERVER_SECRET=scipro_api_secret
- OAUTH2_RESOURCE_SERVER_INTROSPECTION_URI=https://oauth2-${VHOST}/introspect
networks:
- traefik
- internal
labels:
- "traefik.enable=true"
- "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${VHOST}`)"
- "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=letsencrypt"
db:
image: mariadb:10.11
restart: unless-stopped
networks:
- internal
environment:
MARIADB_ROOT_PASSWORD: root
MARIADB_DATABASE: scipro
MARIADB_USER: scipro
MARIADB_PASSWORD: scipro
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect"]
start_period: 10s
interval: 10s
timeout: 5s
retries: 6
oauth2:
build:
context: https://github.com/dsv-su/toker.git
dockerfile: embedded.Dockerfile
restart: unless-stopped
environment:
- CLIENT_ID=scipro_client
- CLIENT_SECRET=scipro_secret
- CLIENT_REDIRECT_URI=https://${VHOST}/login/oauth2/code/scipro
- RESOURCE_SERVER_ID=scipro_api_client
- RESOURCE_SERVER_SECRET=scipro_api_secret
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.oauth2-${COMPOSE_PROJECT_NAME}.rule=Host(`oauth2-${VHOST}`)"
- "traefik.http.routers.oauth2-${COMPOSE_PROJECT_NAME}.tls.certresolver=letsencrypt"
networks:
traefik:
name: traefik
external: true
internal:
name: ${COMPOSE_PROJECT_NAME}_internal

@ -1,7 +1,8 @@
package se.su.dsv.scipro.forum;
import java.io.Serializable;
import java.util.*;
import java.util.List;
import java.util.Set;
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
import se.su.dsv.scipro.forum.dataobjects.ForumThread;
import se.su.dsv.scipro.system.User;
@ -20,4 +21,6 @@ public interface BasicForumService extends Serializable {
List<ForumPost> getPosts(ForumThread forumThread);
ForumThread createThread(String subject);
long countUnreadThreads(List<ForumThread> forumThreadList, User user);
}

@ -87,6 +87,11 @@ public class BasicForumServiceImpl implements BasicForumService {
return threadRepository.save(forumThread);
}
@Override
public long countUnreadThreads(List<ForumThread> forumThreadList, User user) {
return postRepository.countUnreadThreads(forumThreadList, user);
}
private ForumPostReadState getReadState(final User user, final ForumPost post) {
ForumPostReadState state = readStateRepository.find(user, post);
if (state == null) {

@ -8,6 +8,7 @@ import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.JpaRepository;
import se.su.dsv.scipro.system.QueryDslPredicateExecutor;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.Pair;
@Transactional
@ -15,4 +16,6 @@ public interface ForumPostRepository extends JpaRepository<ForumPost, Long>, Que
List<ForumPost> findByThread(ForumThread forumThread);
List<Pair<ProjectThread, ForumPost>> latestPost(Project project, int amount);
long countUnreadThreads(List<ForumThread> forumThreadList, User user);
}

@ -2,19 +2,22 @@ package se.su.dsv.scipro.forum;
import static com.querydsl.core.types.dsl.Expressions.allOf;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQuery;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.persistence.EntityManager;
import java.util.*;
import java.util.List;
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
import se.su.dsv.scipro.forum.dataobjects.ForumThread;
import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
import se.su.dsv.scipro.forum.dataobjects.QForumPost;
import se.su.dsv.scipro.forum.dataobjects.QForumPostReadState;
import se.su.dsv.scipro.forum.dataobjects.QForumThread;
import se.su.dsv.scipro.forum.dataobjects.QProjectThread;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.GenericRepo;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.Pair;
public class ForumPostRepositoryImpl extends GenericRepo<ForumPost, Long> implements ForumPostRepository {
@ -44,4 +47,24 @@ public class ForumPostRepositoryImpl extends GenericRepo<ForumPost, Long> implem
.map(tuple -> new Pair<>(tuple.get(QProjectThread.projectThread), tuple.get(QForumPost.forumPost)))
.toList();
}
@Override
public long countUnreadThreads(List<ForumThread> forumThreadList, User user) {
return new JPAQuery<>(em())
.select(QForumThread.forumThread.id.countDistinct())
.from(QForumThread.forumThread)
.leftJoin(QForumThread.forumThread.posts, QForumPost.forumPost)
.where(
QForumPost.forumPost.notIn(
JPAExpressions.select(QForumPostReadState.forumPostReadState.id.post)
.from(QForumPostReadState.forumPostReadState)
.where(
QForumPostReadState.forumPostReadState.read.isTrue(),
QForumPostReadState.forumPostReadState.id.user.eq(user)
)
),
QForumThread.forumThread.in(forumThreadList)
)
.fetchOne();
}
}

@ -23,5 +23,5 @@ public interface ProjectForumService {
// TODO: Get these away from here
List<Pair<ProjectThread, ForumPost>> latestPost(Project a, int amount);
boolean hasUnreadThreads(Project project, User user);
long getUnreadThreadsCount(Project project, User user);
}

@ -3,7 +3,8 @@ package se.su.dsv.scipro.forum;
import com.google.common.eventbus.EventBus;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.util.*;
import java.util.List;
import java.util.Set;
import se.su.dsv.scipro.file.FileSource;
import se.su.dsv.scipro.file.ProjectFileService;
import se.su.dsv.scipro.forum.dataobjects.ForumPost;
@ -114,14 +115,10 @@ public class ProjectForumServiceImpl implements ProjectForumService {
}
@Override
public boolean hasUnreadThreads(Project project, User user) {
public long getUnreadThreadsCount(Project project, User user) {
List<ProjectThread> threads = getThreads(project);
for (ProjectThread thread : threads) {
if (!basicForumService.isThreadRead(user, thread.getForumThread())) {
return true;
}
}
return false;
List<ForumThread> list = threads.stream().map(ProjectThread::getForumThread).toList();
return basicForumService.countUnreadThreads(list, user);
}
@Override

@ -0,0 +1 @@
DROP TABLE IF EXISTS `grade`;

@ -105,9 +105,9 @@ public class ProjectForumServiceImplTest extends ForumModuleTest {
final ProjectThread thread = service.createThread(project, supervisor, "subject", "content", Set.of());
service.createReply(thread, author, "reply", Set.of());
boolean hasUnreadThreads = service.hasUnreadThreads(project, supervisor);
long count = service.getUnreadThreadsCount(project, supervisor);
assertTrue(hasUnreadThreads);
assertEquals(1, count);
}
private void assertNewForumThread(

@ -72,4 +72,22 @@
</notes>
<cve>CVE-2024-23076</cve>
</suppress>
<suppress>
<notes>
https://nvd.nist.gov/vuln/detail/CVE-2024-49203
https://github.com/querydsl/querydsl/issues/3757
Basically if you allow untrusted user input to be used in the "ORDER BY" clause
you can be vulnerable to SQL injection.
I believe this is nonsense and akin to saying every Java application has a
security vulnerability because JDBC allows you to execute arbitrary SQL if you
do not properly use PreparedStatement with parameters over a string-concatenated
Statement.
Even if this is considered a valid vulnerability we do not, currently, allow
untrusted user input to be used in the "ORDER BY" clause.
</notes>
<cve>CVE-2024-49203</cve>
</suppress>
</suppressions>

@ -6,9 +6,7 @@
</head>
<body>
<wicket:panel>
<a wicket:id="toggle" href="#">
<span wicket:id="icon" class="fa fa-flag read-state"></span>
</a>
<a wicket:id="toggle" href="#"><span wicket:id="icon" class="fa fa-flag read-state"></span></a>
</wicket:panel>
</body>
</html>

@ -10,10 +10,14 @@ import org.apache.wicket.markup.html.panel.Panel;
public abstract class AbstractReadStatePanel extends Panel {
private final Component icon;
public static final String TOGGLE = "toggle";
static final String ICON = "icon";
public AbstractReadStatePanel(final String id) {
super(id);
Component icon = new UpdatingImage(ICON);
icon.setOutputMarkupId(true);
AjaxFallbackLink<Void> link = new AjaxFallbackLink<>(TOGGLE) {
@Override
public void onClick(final Optional<AjaxRequestTarget> target) {
@ -23,20 +27,15 @@ public abstract class AbstractReadStatePanel extends Panel {
});
}
};
add(link);
icon = new UpdatingImage(ICON);
icon.setOutputMarkupId(true);
link.add(icon);
add(link);
}
protected abstract boolean isRead();
protected abstract void onFlagClick(final AjaxRequestTarget target);
public static final String TOGGLE = "toggle";
static final String ICON = "icon";
private class UpdatingImage extends WebComponent {
public UpdatingImage(String id) {

@ -41,6 +41,13 @@
</div>
<table class="table table-striped table-hover" wicket:id="dp"></table>
<wicket:fragment wicket:id="readStateColumnMarkupId">
<span wicket:id="flag"></span>
<wicket:enclosure child="counter">
(<wicket:container wicket:id="counter"></wicket:container>)
</wicket:enclosure>
</wicket:fragment>
</wicket:panel>
</body>
</html>

@ -5,6 +5,7 @@ import static java.util.Arrays.asList;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
import org.apache.wicket.ajax.markup.html.form.AjaxCheckBox;
@ -14,16 +15,22 @@ import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColu
import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
import org.apache.wicket.extensions.markup.html.repeater.data.table.LambdaColumn;
import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.EnumChoiceRenderer;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.LambdaChoiceRenderer;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LambdaModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import se.su.dsv.scipro.components.*;
import se.su.dsv.scipro.components.AjaxCheckBoxMultipleChoice;
import se.su.dsv.scipro.components.BootstrapRadioChoice;
import se.su.dsv.scipro.components.ExportableDataPanel;
import se.su.dsv.scipro.components.ListAdapterModel;
import se.su.dsv.scipro.components.TemporalColumn;
import se.su.dsv.scipro.components.datatables.MultipleUsersColumn;
import se.su.dsv.scipro.components.datatables.UserColumn;
import se.su.dsv.scipro.dataproviders.FilteredDataProvider;
@ -250,25 +257,49 @@ public class SupervisorMyProjectsPanel extends Panel {
@Override
public void populateItem(Item<ICellPopulator<Project>> item, String id, IModel<Project> projectModel) {
item.add(
new AbstractReadStatePanel(id) {
@Override
protected boolean isRead() {
return !projectForumService.hasUnreadThreads(
projectModel.getObject(),
SciProSession.get().getUser()
);
}
// Since table cell only can contain one item, we use Wicket Fragment here. It will contain two components,
// one for flag, one for unread messages counter.
@Override
protected void onFlagClick(AjaxRequestTarget target) {
setResponsePage(
SupervisorThreadedForumPage.class,
SupervisorThreadedForumPage.getPageParameters(projectModel.getObject())
);
}
}
Fragment fragment = new Fragment(id, "readStateColumnMarkupId", SupervisorMyProjectsPanel.this);
long msgCount = projectForumService.getUnreadThreadsCount(
projectModel.getObject(),
SciProSession.get().getUser()
);
boolean isRead = msgCount == 0;
AbstractReadStatePanel readStatePanel = new AbstractReadStatePanel("flag") {
@Override
protected boolean isRead() {
return isRead;
}
@Override
protected void onFlagClick(AjaxRequestTarget target) {
setResponsePage(
SupervisorThreadedForumPage.class,
SupervisorThreadedForumPage.getPageParameters(projectModel.getObject())
);
}
};
if (!isRead) {
readStatePanel.add(new AttributeModifier("title", getString("unread.msg")));
}
fragment.add(readStatePanel);
Label counterLabel = new Label("counter", msgCount) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(msgCount > 0);
}
};
fragment.add(counterLabel);
item.add(fragment);
}
}
}

@ -12,3 +12,5 @@ ProjectStatus.COMPLETED= Completed
SupervisorProjectNoteDisplay.COMPACT=Compact
SupervisorProjectNoteDisplay.FULL=Full
unread.msg=There are unread messages.

@ -12,6 +12,10 @@
<artifactId>war</artifactId>
<packaging>war</packaging>
<properties>
<spring.profile.active>tomcat</spring.profile.active>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
@ -88,7 +92,26 @@
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<propertiesEncoding>${project.build.sourceEncoding}</propertiesEncoding>
<delimiters>
<!-- delimiter for resource filtering is changed since Spring hijacks ${...} -->
<delimiter>@</delimiter>
</delimiters>
<useDefaultDelimiters>false</useDefaultDelimiters>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
@ -110,4 +133,53 @@
</plugins>
</build>
<profiles>
<profile>
<id>branch</id>
<properties>
<spring.profile.active>branch</spring.profile.active>
</properties>
</profile>
<profile>
<id>docker-dependencies</id>
<!--
Some dependencies are not discovered by default when running dependency:go-offline.
They are added here manually to allow Docker build layers to be cached properly.
-->
<dependencies>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml</groupId>
<artifactId>classmate</artifactId>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.minidev</groupId>
<artifactId>json-smart</artifactId>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</profile>
</profiles>
</project>

@ -16,12 +16,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilderCustomizer;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.orm.jpa.SharedEntityManagerCreator;
import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter;
import org.springframework.web.filter.ForwardedHeaderFilter;
import se.su.dsv.scipro.CoreConfig;
import se.su.dsv.scipro.FileSystemStore;
import se.su.dsv.scipro.RepositoryConfiguration;
@ -84,6 +87,22 @@ public class Main extends SpringBootServletInitializer implements ServletContain
return currentProfile;
}
/**
* Spring runs on HTTP and is protected by a HTTPS proxy.
* This filter takes the `X-Forwarded-*` headers and updates the request to reflect the original HTTP request.
* <p>
* Note: This is not needed when we're running behind Apache as a proxy since it uses an AJP connector that has a
* built-in mechanism for handling this,
* see <a href="https://tomcat.apache.org/connectors-doc/common_howto/proxy.html#AJP_as_a_Solution">AJP proxy documentation</a>.
* So this is only for the temporary test servers that are running behind Traefik and use regular HTTP.
*/
@Bean
public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
var filterRegistrationBean = new FilterRegistrationBean<>(new ForwardedHeaderFilter());
filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return filterRegistrationBean;
}
@Bean
public FileStore fileStore() {
return new FileSystemStore();

@ -0,0 +1,19 @@
spring.datasource.url=${JDBC_DATABASE_URL}
spring.datasource.username=${JDBC_DATABASE_USERNAME}
spring.datasource.password=${JDBC_DATABASE_PASSWORD}
profile=DEV
# No secrets available for branch deployment to branch.dsv.su.se
# Will have to set up some mock API for this later
service.grading.url=
oauth.uri=
oauth.clientId=
oauth.clientSecret=
oauth.redirectUri=
# No secrets available for branch deployment to branch.dsv.su.se
# Will have to set up some mock API for this later
daisy.api.url=
daisy.api.username=
daisy.api.password=

@ -0,0 +1 @@
spring.datasource.jndi-name=java:/comp/env/jdbc/sciproDS

@ -1,4 +1,4 @@
spring.datasource.jndi-name=java:/comp/env/jdbc/sciproDS
spring.profiles.active=@spring.profile.active@
spring.flyway.baseline-version=2
spring.flyway.baseline-on-migrate=true
@ -16,17 +16,17 @@ springdoc.swagger-ui.path=/swagger
springdoc.swagger-ui.persist-authorization=true
# These will be overwritten by configuration in the environment of servers it is deployed to
spring.security.oauth2.resourceserver.opaquetoken.client-id=scipro-api-client
spring.security.oauth2.resourceserver.opaquetoken.client-secret=scipro-api-secret
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:59733/introspect
spring.security.oauth2.resourceserver.opaquetoken.client-id=${OAUTH2_RESOURCE_SERVER_ID:scipro-api-client}
spring.security.oauth2.resourceserver.opaquetoken.client-secret=${OAUTH2_RESOURCE_SERVER_SECRET:scipro-api-secret}
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=${OAUTH2_RESOURCE_SERVER_INTROSPECTION_URI:http://localhost:59733/introspect}
# Log in via local OAuth 2 authorization server
spring.security.oauth2.client.provider.docker.user-info-uri=http://localhost:59734/verify
spring.security.oauth2.client.provider.docker.user-info-uri=${OAUTH2_USER_INFO_URI:http://localhost:59734/verify}
spring.security.oauth2.client.provider.docker.user-name-attribute=sub
spring.security.oauth2.client.provider.docker.token-uri=http://localhost:59734/exchange
spring.security.oauth2.client.provider.docker.authorization-uri=http://localhost:59734/authorize
spring.security.oauth2.client.provider.docker.token-uri=${OAUTH2_TOKEN_URI:http://localhost:59734/exchange}
spring.security.oauth2.client.provider.docker.authorization-uri=${OAUTH2_AUTHORIZATION_URI:http://localhost:59734/authorize}
spring.security.oauth2.client.registration.scipro.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.scipro.provider=docker
spring.security.oauth2.client.registration.scipro.client-id=scipro
spring.security.oauth2.client.registration.scipro.client-secret=s3cr3t
spring.security.oauth2.client.registration.scipro.client-id=${OAUTH2_CLIENT_ID:scipro}
spring.security.oauth2.client.registration.scipro.client-secret=${OAUTH2_CLIENT_SECRET:s3cr3t}
spring.security.oauth2.client.registration.scipro.authorization-grant-type=authorization_code