105 Commits

Author SHA1 Message Date
Florian THIERRY
dee473fe46 Add interceptor to inject correlation id in http requests. 2024-09-26 10:38:52 +02:00
Florian THIERRY
5647a5a959 Implementation of jpa saving for traffic traces. 2024-09-26 10:21:20 +02:00
Florian THIERRY
c817371a15 Add skeletton to save traces. 2024-09-25 21:30:25 +02:00
ff52a198dc Upgrade to angular 18. 2024-09-24 22:47:15 +02:00
Florian THIERRY
f3d59a0ef3 conf cleaning. 2024-09-22 13:18:08 +02:00
Florian THIERRY
d84485e52b test 2024-09-22 13:03:39 +02:00
Florian THIERRY
f789d89995 test 2024-09-22 12:53:55 +02:00
Florian THIERRY
7e0174bcc2 test 2024-09-22 12:46:41 +02:00
Florian THIERRY
fe1d59a3bb test 2024-09-22 12:37:01 +02:00
Florian THIERRY
69a99c9312 test 2024-09-22 12:34:02 +02:00
Florian THIERRY
a6414ae64d test 2024-09-22 12:30:18 +02:00
Florian THIERRY
9cf47f0e2a test 2024-09-22 12:27:59 +02:00
Florian THIERRY
6c89562dc3 test 2024-09-22 12:14:04 +02:00
Florian THIERRY
e85eabbed5 test 2024-09-22 12:01:05 +02:00
Florian THIERRY
1ec4ba8212 test conf prod. 2024-09-22 11:10:01 +02:00
Florian THIERRY
a1ff181443 test conf prod. 2024-09-22 11:09:14 +02:00
Florian THIERRY
ee8f48bc43 Fix prod fr configuration. 2024-09-22 10:55:49 +02:00
Florian THIERRY
7ec1aee884 test 2024-09-21 23:39:46 +02:00
Florian THIERRY
a3adfa8ee0 Fix bug of preview content. 2024-09-21 22:20:00 +02:00
Florian THIERRY
d893afa1f3 i18n 2024-09-21 21:43:09 +02:00
Florian THIERRY
d984128176 i18n 2024-09-21 21:40:49 +02:00
Florian THIERRY
f8d73c9ed0 i18n 2024-09-21 21:34:16 +02:00
Florian THIERRY
208b935ffa i18n for some components. 2024-09-21 21:17:31 +02:00
Florian THIERRY
f12dfc7029 i18n for files in core package. 2024-09-21 21:09:36 +02:00
Florian THIERRY
98a890e915 i18n for signin page. 2024-09-21 21:06:47 +02:00
Florian THIERRY
0c1b52d734 i18n for publication search page. 2024-09-21 21:03:34 +02:00
Florian THIERRY
3f6764dd7d i18n for publication update page. 2024-09-21 21:00:59 +02:00
Florian THIERRY
67c3d0b3e6 i18n of publication creation page. 2024-09-21 20:57:05 +02:00
Florian THIERRY
36208ef071 i18n of publication page. 2024-09-21 12:28:55 +02:00
Florian THIERRY
b546a0cf01 Add i18n completion tool. 2024-09-21 12:20:22 +02:00
Florian THIERRY
3935f6ad21 i18n for login page. 2024-09-21 11:22:35 +02:00
Florian THIERRY
fd5ad7e88e i18n for disconnection page. 2024-09-21 11:13:08 +02:00
Florian THIERRY
8c957fe694 i18n for home page. 2024-09-21 11:12:07 +02:00
Florian THIERRY
cb0ef7ddd5 Test 2024-09-20 18:38:17 +02:00
Florian THIERRY
42e466fe8b test ci. 2024-09-20 17:32:43 +02:00
Florian THIERRY
2c5fa4fa13 test ci. 2024-09-20 17:25:37 +02:00
Florian THIERRY
ebce44c889 test ci 2024-09-20 17:12:13 +02:00
Florian THIERRY
955dc48f51 test ci 2024-09-20 16:46:11 +02:00
Florian THIERRY
29e75e6298 Test ci. 2024-09-20 16:34:29 +02:00
Florian THIERRY
187fd105d3 Fix pictures paths. 2024-09-20 09:36:10 +02:00
Florian THIERRY
1b92fd269e Fix config. 2024-09-19 22:24:39 +02:00
Florian THIERRY
23025e3606 Fix fucking MIME types of javascript files. 2024-09-19 22:13:10 +02:00
Florian THIERRY
38c11e2d9f gitignore. 2024-09-19 18:03:33 +02:00
Florian THIERRY
610723c561 CI/CD drafting. 2024-09-19 18:03:23 +02:00
Florian THIERRY
0e2fb945a4 Initialize docker build configuration. 2024-09-18 10:18:40 +02:00
Florian THIERRY
ef32572521 Change favicon. 2024-09-18 09:22:11 +02:00
Florian THIERRY
e5a128a7f6 Fix style. 2024-09-18 09:10:46 +02:00
Florian THIERRY
f5e1e10ebd Add confirmation dialog to delete publications. 2024-09-18 09:05:18 +02:00
Florian THIERRY
500952d4d4 Implementation of categoryId edition. 2024-09-10 10:47:58 +02:00
Florian THIERRY
1cc4abc24e Add default picture and factorization of title in publication edition. 2024-09-10 09:40:45 +02:00
Florian THIERRY
00945de270 Code cleaning about publication edition component. 2024-09-06 10:20:05 +02:00
Florian THIERRY
5804d8cc9f Extract publication edition into a separated component. 2024-09-05 17:28:30 +02:00
Florian THIERRY
5610bd170a Minor changes 2024-09-05 09:35:46 +02:00
Florian THIERRY
21d19d4ecd Styling elements. 2024-09-05 09:33:07 +02:00
Florian THIERRY
f3dfac6bc7 Add delete button. 2024-09-05 09:23:25 +02:00
Florian THIERRY
4565192d0b Remote temp configuration. 2024-09-04 14:20:31 +02:00
Florian THIERRY
d7ac4966c9 Add "new publication" button. 2024-09-04 14:19:26 +02:00
Florian THIERRY
64119a956a Add "my-publications" page. 2024-09-04 14:06:10 +02:00
Florian THIERRY
c4dea2cc85 Dependency cleaning. 2024-09-04 10:28:22 +02:00
Florian THIERRY
b0cc42fddd Implementaiton of preview tab. 2024-09-04 10:26:47 +02:00
Florian THIERRY
db669114b2 Add Prism.js. 2024-09-04 09:29:10 +02:00
Florian THIERRY
ca6b207816 Fix refresh token mecanism. 2024-09-04 08:58:47 +02:00
Florian THIERRY
b091dc52b7 Fix authentication errors handling. 2024-09-03 11:45:47 +02:00
Florian THIERRY
4d44b6f53c First implementation of refresh token. 2024-09-03 10:37:58 +02:00
Florian THIERRY
be34c555a5 Implementation of code-block addition in the publication. 2024-09-03 10:07:13 +02:00
Florian THIERRY
c03d977028 Add code-block dialog and style it. 2024-09-03 10:00:45 +02:00
Florian THIERRY
afd184f936 Fix author-id publications search. 2024-08-30 22:37:03 +02:00
Florian THIERRY
c09c68e1ac Implementation of picture addition button in editor. 2024-08-30 18:13:43 +02:00
Florian THIERRY
b84ba15f4c Implementaiton of h1, h2, h3 and link button on publication editor. 2024-08-30 18:01:29 +02:00
Florian THIERRY
51af25666d Fix search bar navigation. 2024-08-30 09:59:21 +02:00
Florian THIERRY
b3a52f6a4b Extract search bar into a standalone component and fix header design. 2024-08-30 09:52:00 +02:00
Florian THIERRY
090143fdae Add search publication page and fix category access 2024-08-29 17:24:01 +02:00
Florian THIERRY
b5f881e2c5 Add a component to display a publication list and fix publication search rest service to handle ids. 2024-08-29 13:56:14 +02:00
Florian THIERRY
d9b856bd43 Fix categories loading in side menu component. 2024-08-29 11:09:39 +02:00
Florian THIERRY
5e4068b141 Add style and help button. 2024-08-29 10:20:31 +02:00
Florian THIERRY
a2f1b511c1 Add buttons in publication editor. 2024-08-25 22:40:11 +02:00
Florian THIERRY
4d20d5f8b8 Add button to edit publications on publication view page. 2024-08-25 12:02:52 +02:00
Florian THIERRY
f00fb103ba Add submit button component and style it. 2024-08-22 10:26:38 +02:00
Florian THIERRY
b1d9344574 Styling picture selection components. 2024-08-19 23:02:39 +02:00
Florian THIERRY
56ac024cba Mess commit. 2024-08-19 22:42:42 +02:00
Florian THIERRY
32ab1d79c8 Add publication edition form. 2024-08-04 15:54:00 +02:00
Florian THIERRY
42c4f76c0d Design some buttons in header. 2024-06-28 09:35:56 +02:00
Florian THIERRY
4cc2a15231 Responsive styling of header. 2024-06-12 13:36:16 +02:00
Florian THIERRY
54fbc7d609 Adding css transitions. 2024-06-12 13:22:20 +02:00
Florian THIERRY
8e9440a104 Responsive styling of publication page. 2024-06-12 13:19:08 +02:00
Florian THIERRY
00d49d5fa4 Responsive styling of home page. 2024-06-12 13:15:03 +02:00
Florian THIERRY
78325c8729 Fix footer at page bottom. 2024-06-12 12:38:42 +02:00
Florian THIERRY
1e18e3bc52 Add signin page. 2024-06-11 12:55:11 +02:00
Florian THIERRY
8ada2a15ef Styling login form. 2024-06-07 09:44:51 +02:00
Florian THIERRY
95d5308934 Re-design of login page. 2024-06-06 11:05:02 +02:00
Florian THIERRY
e5076f0c64 Design header search bar. 2024-06-05 10:23:38 +02:00
Florian THIERRY
2ba707c336 Fix home page design. 2024-06-05 10:05:02 +02:00
Florian THIERRY
d3041cf03d Styling publications on home page. 2024-06-04 13:55:26 +02:00
Florian THIERRY
58295398e0 Add endpoint to retrieve latest publications. 2024-06-04 13:06:20 +02:00
Florian THIERRY
067bf7885a Update side menu header. 2024-06-04 12:55:46 +02:00
Florian THIERRY
d324b94ddb Add SQL script to rebuild categories hierarchy. 2024-04-22 16:05:59 +02:00
Florian THIERRY
4985889c58 Styling header. 2024-04-22 16:05:35 +02:00
Florian THIERRY
7f5d52dce5 Add side menu for header. 2024-04-22 15:57:22 +02:00
Florian THIERRY
fae709a254 Fix class not found error. 2024-04-22 14:37:20 +02:00
Florian THIERRY
45355f6c42 Refactor publication parser location. 2024-04-22 14:22:36 +02:00
Florian THIERRY
db492b6316 Add parsed text to publication entities. 2024-04-22 14:13:17 +02:00
Florian THIERRY
c54e1c57d7 Creation of side-menu. 2024-04-02 16:18:03 +02:00
Florian THIERRY
0900df463a Add disconnection and minor improvements on login page. 2024-03-27 12:15:41 +01:00
Florian THIERRY
13c2cc8118 Add frontend. 2024-03-27 10:28:33 +01:00
Florian THIERRY
431d365d20 Move backend files into a sub folder. 2024-03-27 10:28:22 +01:00
288 changed files with 25156 additions and 259 deletions

46
.gitignore vendored
View File

@@ -38,3 +38,49 @@ build/
**/.angular **/.angular
**/pictures-folder **/pictures-folder
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
**/ci/bin/

15
Dockerfile-backend Normal file
View File

@@ -0,0 +1,15 @@
FROM maven:3.9.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY backend/pom.xml /app/
COPY backend/codiki-application /app/codiki-application
COPY backend/codiki-domain /app/codiki-domain
COPY backend/codiki-exposition /app/codiki-exposition
COPY backend/codiki-infrastructure /app/codiki-infrastructure
COPY backend/codiki-launcher /app/codiki-launcher
WORKDIR /app
RUN mvn clean install -N
RUN mvn clean package
FROM eclipse-temurin:21-jre-alpine AS final
COPY --from=builder /app/codiki-launcher/target/*.jar /app/codiki.jar
CMD ["java", "-jar", "/app/codiki.jar"]

12
Dockerfile-frontend Normal file
View File

@@ -0,0 +1,12 @@
FROM node:22-alpine AS builder
WORKDIR /app
COPY frontend /app
RUN npm install
RUN npm run build-prod-en
RUN npm run build-prod-fr
FROM nginx:1.27-alpine AS final
WORKDIR /app
COPY --from=builder /app/dist/codiki/en/browser /usr/share/nginx/html/en/
COPY --from=builder /app/dist/codiki/fr/browser/fr /usr/share/nginx/html/fr/
COPY frontend/conf/nginx.conf /etc/nginx/nginx.conf

84
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,84 @@
pipeline {
agent any
stages {
stage('Configuration') {
steps {
dir('codiki-properties') {
git url:'https://gitea.takiguchi.ovh/Codiki/codiki-properties.git', branch: 'hexagonal-reforged', credentialsId: 'Jenkins-gitea'
}
script {
sh 'sed -i "s/<POSTGRES_PASSWORD>/$(cat ./codiki-properties/passwords/postgresql/codiki_user)/g" ./codiki-properties/application-prod.yml'
sh 'cp ./codiki-properties/application-prod.yml ./backend/codiki-launcher/src/main/resources/application-prod.yml'
sh 'sed -i "s/<POSTGRES_PASSWORD>/$(cat ./codiki-properties/passwords/postgresql/codiki_admin)/g" ./docker-compose.yml'
}
}
}
stage('Build') {
steps {
script {
sh """
sudo /usr/bin/docker build -t codiki-backend -f ./Dockerfile-backend . --no-cache
sudo /usr/bin/docker build -t codiki-frontend -f ./Dockerfile-frontend . --no-cache
sudo /usr/bin/docker save codiki-backend:latest -o ./codiki-backend.tar
sudo /usr/bin/docker save codiki-frontend:latest -o ./codiki-frontend.tar
sudo chown jenkins:jenkins ./codiki-backend.tar
sudo chown jenkins:jenkins ./codiki-frontend.tar
chmod 644 ./codiki-backend.tar
chmod 644 ./codiki-frontend.tar
"""
}
}
}
stage('Deploy') {
steps {
sshPublisher(
publishers: [
sshPublisherDesc(
configName: 'DebianServer [codiki]',
transfers: [
sshTransfer(
cleanRemote: false,
excludes: '',
execCommand: '',
execTimeout: 120000,
flatten: false,
makeEmptyDirs: false,
noDefaultExcludes: false,
patternSeparator: '[, ]+',
remoteDirectory: '',
remoteDirectorySDF: false,
removePrefix: '',
sourceFiles: 'codiki-backend.tar,codiki-frontend.tar,docker-compose.yml'
),
sshTransfer(
cleanRemote: false,
excludes: '',
execCommand: """
cd /opt/codiki
sudo /usr/bin/docker load < /opt/codiki/codiki-backend.tar
sudo /usr/bin/docker load < /opt/codiki/codiki-frontend.tar
sudo /usr/bin/docker compose down
sudo /usr/bin/docker compose up --detach
""",
execTimeout: 120000,
flatten: false,
makeEmptyDirs: false,
noDefaultExcludes: false,
patternSeparator: '[, ]+',
remoteDirectory: '',
remoteDirectorySDF: false,
removePrefix: '',
sourceFiles: ''
)
],
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: true
)
]
)
}
}
}
}

View File

@@ -33,6 +33,10 @@
<groupId>com.auth0</groupId> <groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId> <artifactId>java-jwt</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId> <artifactId>junit-jupiter-api</artifactId>

View File

@@ -1,6 +1,8 @@
package org.codiki.application.picture; package org.codiki.application.picture;
import java.io.File; import java.io.File;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@@ -30,6 +32,7 @@ public class PictureUseCases {
.withId(UUID.randomUUID()) .withId(UUID.randomUUID())
.withPublisher(authenticatedUser) .withPublisher(authenticatedUser)
.withContentFile(pictureFile) .withContentFile(pictureFile)
.withPublicationDate(ZonedDateTime.now())
.build(); .build();
picturePort.save(newPicture); picturePort.save(newPicture);
@@ -48,4 +51,11 @@ public class PictureUseCases {
public boolean existsById(UUID pictureId) { public boolean existsById(UUID pictureId) {
return picturePort.existsById(pictureId); return picturePort.existsById(pictureId);
} }
public List<Picture> getAllOfCurrentUser() {
User authenticatedUser = userUseCases.getAuthenticatedUser()
.orElseThrow(AuthenticationRequiredException::new);
return picturePort.findAllByPublisherId(authenticatedUser.id());
}
} }

View File

@@ -0,0 +1,144 @@
package org.codiki.application.publication;
import org.springframework.stereotype.Service;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4;
@Service
public class ParserService {
private static final String REG_CODE = "\\[code lg=&quot;([a-z]+)&quot;\\](.*)\\[\\/code\\]\\n";
private static final String REG_IMAGES = "\\[img src=&quot;([^\"| ]+)&quot;( alt=&quot;([^\"| ]+)&quot;)? \\/\\]";
private static final String REG_LINKS = "\\[link href=&quot;([^\"| ]+)&quot; txt=&quot;([^\"| ]+)&quot; \\/\\]";
private static final Pattern PATTERN_CODE;
private static final Pattern PATTERN_IMAGES;
private static final Pattern PATTERN_LINKS;
static {
PATTERN_CODE = Pattern.compile(REG_CODE);
PATTERN_IMAGES = Pattern.compile(REG_IMAGES);
PATTERN_LINKS = Pattern.compile(REG_LINKS);
}
public String parse(String pSource) {
return unParseDolars(parseCode(parseHeaders(parseImages(parseLinks(parseBackSpaces(escapeHtml4(parseDolars(pSource))))))));
}
private String parseDolars(final String pSource) {
return pSource.replace("$", "£ø");
}
private String unParseDolars(final String pSource) {
return pSource.replace("&pound;&oslash;", "$");
}
String parseHeaders(final String pSource) {
String result = pSource;
for(int i = 1 ; i <= 3 ; i++) {
result = parseHeadersHX(result, i);
}
return result;
}
String parseHeadersHX(final String pSource, final int pNumHeader) {
String result = pSource;
// (.*)(\[hX\](.*)\[\/hX\])+(.*)
final String regex = concat("(.*)(\\[h", pNumHeader, "\\](.*)\\[\\/h", pNumHeader, "\\])+(.*)");
final Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(result);
while(matcher.find()) {
// \1<hX>\3</hX>\4
result = matcher.replaceFirst(concat(matcher.group(1),
"<h", pNumHeader, ">", matcher.group(3), "</h", pNumHeader, ">", matcher.group(4)));
matcher = pattern.matcher(result);
}
return result;
}
String parseBackSpaces(final String pSource) {
return pSource.replaceAll("\r?\n", "<br/>").replaceAll("\\[\\/code\\]<br\\/><br\\/>", "[/code]\n");
}
String parseImages(final String pSource) {
String result = pSource;
Matcher matcher = PATTERN_IMAGES.matcher(result);
while(matcher.find()) {
String altStr = matcher.group(3);
if(altStr == null) {
altStr = "";
}
result = matcher.replaceFirst(concat("<img src=\"", matcher.group(1), "\" alt=\"", altStr, "\" />"));
matcher = PATTERN_IMAGES.matcher(result);
}
return result;
}
String parseLinks(final String pSource) {
String result = pSource;
Matcher matcher = PATTERN_LINKS.matcher(result);
while(matcher.find()) {
result = matcher.replaceFirst(concat("<a href=\"", matcher.group(1), "\">", matcher.group(2), "</a>"));
matcher = PATTERN_LINKS.matcher(result);
}
return result;
}
protected String parseCode(final String pSource) {
String result = pSource;
Matcher matcher = PATTERN_CODE.matcher(result);
while(matcher.find()) {
// replace the '<br/>' in group by '\n'
String codeContent = matcher.group(2).replaceAll("<br\\/>", "\n");
if(codeContent.startsWith("\n")) {
codeContent = codeContent.substring(1);
}
result = matcher.replaceFirst(
concat(
"<pre class=\"line-numbers\"><code class=\"language-",
matcher.group(1),
"\">",
codeContent,
"</code></pre>"
)
);
matcher = PATTERN_CODE.matcher(result);
}
return result;
}
/**
* Concatenate the parameters to form just one single string.
*
* @param pArgs
* The strings to concatenate.
* @return The parameters concatenated.
*/
private static String concat(final Object... pArgs) {
final StringBuilder result = new StringBuilder();
for (final Object arg : pArgs) {
result.append(arg);
}
return result.toString();
}
}

View File

@@ -3,12 +3,15 @@ package org.codiki.application.publication;
import static java.util.Objects.isNull; import static java.util.Objects.isNull;
import java.time.Clock; import java.time.Clock;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import static org.codiki.domain.publication.model.builder.AuthorBuilder.anAuthor; import static org.codiki.domain.publication.model.builder.AuthorBuilder.anAuthor;
import static org.codiki.domain.publication.model.builder.PublicationBuilder.aPublication; import static org.codiki.domain.publication.model.builder.PublicationBuilder.aPublication;
import static org.springframework.util.ObjectUtils.isEmpty;
import org.codiki.application.category.CategoryUseCases; import org.codiki.application.category.CategoryUseCases;
import org.codiki.application.picture.PictureUseCases; import org.codiki.application.picture.PictureUseCases;
import org.codiki.application.user.UserUseCases; import org.codiki.application.user.UserUseCases;
@@ -31,6 +34,7 @@ public class PublicationUseCases {
private final CategoryUseCases categoryUseCases; private final CategoryUseCases categoryUseCases;
private final Clock clock; private final Clock clock;
private final KeyGenerator keyGenerator; private final KeyGenerator keyGenerator;
private final ParserService parserService;
private final PictureUseCases pictureUseCases; private final PictureUseCases pictureUseCases;
private final PublicationCreationRequestValidator publicationCreationRequestValidator; private final PublicationCreationRequestValidator publicationCreationRequestValidator;
private final PublicationPort publicationPort; private final PublicationPort publicationPort;
@@ -42,6 +46,7 @@ public class PublicationUseCases {
CategoryUseCases categoryUseCases, CategoryUseCases categoryUseCases,
Clock clock, Clock clock,
KeyGenerator keyGenerator, KeyGenerator keyGenerator,
ParserService parserService,
PictureUseCases pictureUseCases, PictureUseCases pictureUseCases,
PublicationCreationRequestValidator publicationCreationRequestValidator, PublicationCreationRequestValidator publicationCreationRequestValidator,
PublicationPort publicationPort, PublicationPort publicationPort,
@@ -52,6 +57,7 @@ public class PublicationUseCases {
this.categoryUseCases = categoryUseCases; this.categoryUseCases = categoryUseCases;
this.clock = clock; this.clock = clock;
this.keyGenerator = keyGenerator; this.keyGenerator = keyGenerator;
this.parserService = parserService;
this.publicationCreationRequestValidator = publicationCreationRequestValidator; this.publicationCreationRequestValidator = publicationCreationRequestValidator;
this.publicationPort = publicationPort; this.publicationPort = publicationPort;
this.publicationUpdateRequestValidator = publicationUpdateRequestValidator; this.publicationUpdateRequestValidator = publicationUpdateRequestValidator;
@@ -83,6 +89,7 @@ public class PublicationUseCases {
.withKey(keyGenerator.generateKey()) .withKey(keyGenerator.generateKey())
.withTitle(request.title()) .withTitle(request.title())
.withText(request.text()) .withText(request.text())
.withParsedText(parserService.parse(request.text()))
.withDescription(request.description()) .withDescription(request.description())
.withCreationDate(ZonedDateTime.now(clock)) .withCreationDate(ZonedDateTime.now(clock))
.withIllustrationId(request.illustrationId()) .withIllustrationId(request.illustrationId())
@@ -116,6 +123,7 @@ public class PublicationUseCases {
if (!isNull(request.text())) { if (!isNull(request.text())) {
publicationBuilder.withText(request.text()); publicationBuilder.withText(request.text());
publicationBuilder.withParsedText(parserService.parse(request.text()));
} }
if (!isNull(request.description())) { if (!isNull(request.description())) {
@@ -163,7 +171,19 @@ public class PublicationUseCases {
} }
public Optional<Publication> findById(UUID publicationId) { public Optional<Publication> findById(UUID publicationId) {
return publicationPort.findById(publicationId); return publicationPort.findById(publicationId)
.map(publication -> {
Publication result = publication;
if (isEmpty(publication.parsedText())) {
Publication editedPublication = aPublication()
.basedOn(publication)
.withParsedText(parserService.parse(publication.text()))
.build();
publicationPort.save(editedPublication);
result = editedPublication;
}
return result;
});
} }
public List<Publication> searchPublications(String searchQuery) { public List<Publication> searchPublications(String searchQuery) {
@@ -171,4 +191,12 @@ public class PublicationUseCases {
return publicationPort.search(criteria); return publicationPort.search(criteria);
} }
public List<Publication> getLatest() {
return publicationPort.getLatest();
}
public String previewContent(String publicationText) {
return parserService.parse(publicationText);
}
} }

View File

@@ -25,6 +25,10 @@ public class CustomUserDetails implements UserDetails {
.toList(); .toList();
} }
public User getUser() {
return user;
}
@Override @Override
public String getUsername() { public String getUsername() {
return user.id().toString(); return user.id().toString();

View File

@@ -0,0 +1,47 @@
package org.codiki.application.traffic;
import jakarta.annotation.Nullable;
import org.codiki.domain.traffic.exception.TrafficTraceCreationException;
import org.codiki.domain.traffic.model.TrafficEndpoint;
import org.codiki.domain.traffic.model.TrafficTrace;
import org.codiki.domain.traffic.port.TrafficTracePort;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.time.Clock;
import java.time.ZonedDateTime;
import java.util.UUID;
import static java.util.Objects.isNull;
import static org.codiki.domain.traffic.model.TrafficTrace.aTrafficTrace;
@Component
public class TrafficTraceUseCases {
private final TrafficTracePort trafficTracePort;
private final Clock clock;
public TrafficTraceUseCases(TrafficTracePort trafficTracePort, Clock clock) {
this.trafficTracePort = trafficTracePort;
this.clock = clock;
}
@Async
public void saveNewTrace(
TrafficEndpoint trafficEndpoint,
@Nullable UUID userId,
@Nullable String correlationId
) {
if (isNull(trafficEndpoint)) {
throw new TrafficTraceCreationException("Traffic endpoint should not be null.");
}
TrafficTrace newTrace = aTrafficTrace()
.withId(UUID.randomUUID())
.withDateTime(ZonedDateTime.now(clock))
.withEndpoint(trafficEndpoint)
.withUserId(userId)
.withCorrelationId(correlationId)
.build();
trafficTracePort.save(newTrace);
}
}

View File

@@ -87,9 +87,7 @@ public class UserUseCases {
.map(Authentication::getPrincipal) .map(Authentication::getPrincipal)
.filter(CustomUserDetails.class::isInstance) .filter(CustomUserDetails.class::isInstance)
.map(CustomUserDetails.class::cast) .map(CustomUserDetails.class::cast)
.map(CustomUserDetails::getUsername) .map(CustomUserDetails::getUser);
.map(UUID::fromString)
.flatMap(userPort::findById);
} }
private UserAuthenticationData generateAuthenticationData(User user) { private UserAuthenticationData generateAuthenticationData(User user) {

View File

@@ -5,6 +5,7 @@ import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
class KeyGeneratorTest { class KeyGeneratorTest {
@@ -16,6 +17,7 @@ class KeyGeneratorTest {
} }
@Test @Test
@Disabled
public void generateKey_should_generate_random_keys_with_alphanumeric_characters() { public void generateKey_should_generate_random_keys_with_alphanumeric_characters() {
Pattern validationRegex = Pattern.compile("^[0-9A-Z]{10}$"); Pattern validationRegex = Pattern.compile("^[0-9A-Z]{10}$");

View File

@@ -1,10 +1,12 @@
package org.codiki.domain.picture.model; package org.codiki.domain.picture.model;
import java.io.File; import java.io.File;
import java.time.ZonedDateTime;
import java.util.UUID; import java.util.UUID;
public record Picture( public record Picture(
UUID id, UUID id,
UUID publisherId, UUID publisherId,
ZonedDateTime publishedAt,
File contentFile File contentFile
) {} ) {}

View File

@@ -1,6 +1,7 @@
package org.codiki.domain.picture.model.builder; package org.codiki.domain.picture.model.builder;
import java.io.File; import java.io.File;
import java.time.ZonedDateTime;
import java.util.UUID; import java.util.UUID;
import org.codiki.domain.picture.model.Picture; import org.codiki.domain.picture.model.Picture;
@@ -9,6 +10,7 @@ import org.codiki.domain.user.model.User;
public class PictureBuilder { public class PictureBuilder {
private UUID id; private UUID id;
private UUID publisherId; private UUID publisherId;
private ZonedDateTime publishedAt;
private File contentFile; private File contentFile;
private PictureBuilder() {} private PictureBuilder() {}
@@ -37,12 +39,17 @@ public class PictureBuilder {
return withPublisherId(publisher.id()); return withPublisherId(publisher.id());
} }
public PictureBuilder withPublicationDate(ZonedDateTime publishedAt) {
this.publishedAt = publishedAt;
return this;
}
public PictureBuilder withContentFile(File contentFile) { public PictureBuilder withContentFile(File contentFile) {
this.contentFile = contentFile; this.contentFile = contentFile;
return this; return this;
} }
public Picture build() { public Picture build() {
return new Picture(id, publisherId, contentFile); return new Picture(id, publisherId, publishedAt, contentFile);
} }
} }

View File

@@ -1,5 +1,6 @@
package org.codiki.domain.picture.port; package org.codiki.domain.picture.port;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@@ -13,4 +14,6 @@ public interface PicturePort {
void save(Picture picture); void save(Picture picture);
void deleteById(UUID pictureId); void deleteById(UUID pictureId);
List<Picture> findAllByPublisherId(UUID id);
} }

View File

@@ -0,0 +1,9 @@
package org.codiki.domain.publication.exception;
import org.codiki.domain.exception.FunctionnalException;
public class BadPublicationSearchCriterionException extends FunctionnalException {
public BadPublicationSearchCriterionException(String message) {
super(message);
}
}

View File

@@ -5,7 +5,7 @@ import java.util.UUID;
public record Author( public record Author(
UUID id, UUID id,
String name, String name,
String image String photoId
) { ) {
} }

View File

@@ -8,6 +8,7 @@ public record Publication(
String key, String key,
String title, String title,
String text, String text,
String parsedText,
String description, String description,
ZonedDateTime creationDate, ZonedDateTime creationDate,
UUID illustrationId, UUID illustrationId,

View File

@@ -40,6 +40,7 @@ public class AuthorBuilder {
} }
public Author build() { public Author build() {
//
return new Author(id, name, image); return new Author(id, name, image);
} }
} }

View File

@@ -1,17 +1,17 @@
package org.codiki.domain.publication.model.builder; package org.codiki.domain.publication.model.builder;
import org.codiki.domain.publication.model.Author;
import org.codiki.domain.publication.model.Publication;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.UUID; import java.util.UUID;
import org.codiki.domain.category.model.Category;
import org.codiki.domain.publication.model.Author;
import org.codiki.domain.publication.model.Publication;
public class PublicationBuilder { public class PublicationBuilder {
private UUID id; private UUID id;
private String key; private String key;
private String title; private String title;
private String text; private String text;
private String parsedText;
private String description; private String description;
private ZonedDateTime creationDate; private ZonedDateTime creationDate;
private UUID illustrationId; private UUID illustrationId;
@@ -30,6 +30,7 @@ public class PublicationBuilder {
.withKey(publication.key()) .withKey(publication.key())
.withTitle(publication.title()) .withTitle(publication.title())
.withText(publication.text()) .withText(publication.text())
.withParsedText(publication.parsedText())
.withDescription(publication.description()) .withDescription(publication.description())
.withCreationDate(publication.creationDate()) .withCreationDate(publication.creationDate())
.withIllustrationId(publication.illustrationId()) .withIllustrationId(publication.illustrationId())
@@ -57,6 +58,11 @@ public class PublicationBuilder {
return this; return this;
} }
public PublicationBuilder withParsedText(String parsedText) {
this.parsedText = parsedText;
return this;
}
public PublicationBuilder withDescription(String description) { public PublicationBuilder withDescription(String description) {
this.description = description; this.description = description;
return this; return this;
@@ -88,6 +94,7 @@ public class PublicationBuilder {
key, key,
title, title,
text, text,
parsedText,
description, description,
creationDate, creationDate,
illustrationId, illustrationId,

View File

@@ -15,4 +15,6 @@ public interface PublicationPort {
void delete(Publication publication); void delete(Publication publication);
List<Publication> search(List<PublicationSearchCriterion> criteria); List<Publication> search(List<PublicationSearchCriterion> criteria);
List<Publication> getLatest();
} }

View File

@@ -0,0 +1,9 @@
package org.codiki.domain.traffic.exception;
import org.codiki.domain.exception.FunctionnalException;
public class TrafficTraceCreationException extends FunctionnalException {
public TrafficTraceCreationException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,14 @@
package org.codiki.domain.traffic.model;
import java.util.Arrays;
import java.util.Optional;
public enum HttpMethod {
GET, POST, PUT, DELETE;
public static Optional<HttpMethod> fromString(String methodAsString) {
return Arrays.stream(values())
.filter(method -> method.name().equals(methodAsString))
.findFirst();
}
}

View File

@@ -0,0 +1,6 @@
package org.codiki.domain.traffic.model;
public record TrafficEndpoint(
HttpMethod method,
String path
) {}

View File

@@ -0,0 +1,55 @@
package org.codiki.domain.traffic.model;
import java.time.ZonedDateTime;
import java.util.UUID;
public record TrafficTrace(
UUID id,
ZonedDateTime dateTime,
TrafficEndpoint endpoint,
UUID userId,
String correlationId
) {
public static Builder aTrafficTrace() {
return new Builder();
}
public static class Builder {
private UUID id;
private ZonedDateTime dateTime;
private TrafficEndpoint endpoint;
private UUID userId;
private String correlationId;
private Builder() {}
public Builder withId(UUID id) {
this.id = id;
return this;
}
public Builder withDateTime(ZonedDateTime dateTime) {
this.dateTime = dateTime;
return this;
}
public Builder withEndpoint(TrafficEndpoint endpoint) {
this.endpoint = endpoint;
return this;
}
public Builder withUserId(UUID userId) {
this.userId = userId;
return this;
}
public Builder withCorrelationId(String correlationId) {
this.correlationId = correlationId;
return this;
}
public TrafficTrace build() {
return new TrafficTrace(id, dateTime, endpoint, userId, correlationId);
}
}
}

View File

@@ -0,0 +1,14 @@
package org.codiki.domain.traffic.port;
import org.codiki.domain.traffic.model.TrafficTrace;
import java.time.ZonedDateTime;
import java.util.List;
public interface TrafficTracePort {
void save(TrafficTrace trace);
List<TrafficTrace> getAllInPeriod(ZonedDateTime startDate, ZonedDateTime endDate);
List<TrafficTrace> getAllByCorrelationId(String correlationId);
Integer countAllInPeriod(ZonedDateTime startDate, ZonedDateTime endDate);
Integer countByCorrelationId(String correlationId);
}

View File

@@ -25,6 +25,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
@@ -33,28 +37,5 @@
<groupId>org.apache.tika</groupId> <groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId> <artifactId>tika-core</artifactId>
</dependency> </dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-data-jpa</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-security</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.postgresql</groupId>-->
<!-- <artifactId>postgresql</artifactId>-->
<!-- <scope>runtime</scope>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-test</artifactId>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.security</groupId>-->
<!-- <artifactId>spring-security-test</artifactId>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
</dependencies> </dependencies>
</project> </project>

View File

@@ -7,16 +7,10 @@ import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import org.codiki.domain.category.exception.CategoryDeletionException; import org.codiki.domain.category.exception.CategoryDeletionException;
import org.codiki.domain.category.exception.CategoryEditionException; import org.codiki.domain.category.exception.CategoryEditionException;
import org.codiki.domain.category.exception.CategoryNotFoundException; import org.codiki.domain.category.exception.CategoryNotFoundException;
import org.codiki.domain.exception.LoginFailureException; import org.codiki.domain.exception.*;
import org.codiki.domain.exception.RefreshTokenDoesNotExistException;
import org.codiki.domain.exception.RefreshTokenExpiredException;
import org.codiki.domain.exception.UserDoesNotExistException;
import org.codiki.domain.picture.exception.PictureNotFoundException; import org.codiki.domain.picture.exception.PictureNotFoundException;
import org.codiki.domain.picture.exception.PictureUploadException; import org.codiki.domain.picture.exception.PictureUploadException;
import org.codiki.domain.publication.exception.NoPublicationSearchResultException; import org.codiki.domain.publication.exception.*;
import org.codiki.domain.publication.exception.PublicationEditionException;
import org.codiki.domain.publication.exception.PublicationNotFoundException;
import org.codiki.domain.publication.exception.PublicationUpdateForbiddenException;
import org.codiki.domain.user.exception.UserAlreadyExistsException; import org.codiki.domain.user.exception.UserAlreadyExistsException;
import org.codiki.domain.user.exception.UserCreationException; import org.codiki.domain.user.exception.UserCreationException;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@@ -28,6 +22,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep
@RestControllerAdvice @RestControllerAdvice
public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHandler { public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({ @ExceptionHandler({
BadPublicationSearchCriterionException.class,
CategoryDeletionException.class, CategoryDeletionException.class,
CategoryEditionException.class, CategoryEditionException.class,
CategoryNotFoundException.class, CategoryNotFoundException.class,
@@ -53,7 +48,8 @@ public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHan
} }
@ExceptionHandler({ @ExceptionHandler({
RefreshTokenExpiredException.class RefreshTokenExpiredException.class,
AuthenticationRequiredException.class,
}) })
public ProblemDetail handleUnauthorizedExceptions(Exception exception) { public ProblemDetail handleUnauthorizedExceptions(Exception exception) {
return buildProblemDetail(UNAUTHORIZED, exception); return buildProblemDetail(UNAUTHORIZED, exception);

View File

@@ -0,0 +1,12 @@
package org.codiki.exposition.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAspectJAutoProxy
@EnableAsync
public class TrafficTraceConfiguration {
}

View File

@@ -38,10 +38,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
.filter(authorizationHeader -> !isEmpty(authorizationHeader)) .filter(authorizationHeader -> !isEmpty(authorizationHeader))
.filter(authorizationHeader -> authorizationHeader.startsWith(BEARER_PREFIX)) .filter(authorizationHeader -> authorizationHeader.startsWith(BEARER_PREFIX))
.map(authorizationHeader -> authorizationHeader.substring(BEARER_PREFIX.length())) .map(authorizationHeader -> authorizationHeader.substring(BEARER_PREFIX.length()))
.filter(token -> {
String authorizationHeader = request.getHeader(AUTHORIZATION);
return !isEmpty(authorizationHeader) && authorizationHeader.startsWith(BEARER_PREFIX);
})
.filter(jwtService::isValid) .filter(jwtService::isValid)
.flatMap(jwtService::extractUser) .flatMap(jwtService::extractUser)
.map(CustomUserDetails::new) .map(CustomUserDetails::new)

View File

@@ -48,12 +48,14 @@ public class SecurityConfiguration {
"/api/pictures/{pictureId}", "/api/pictures/{pictureId}",
"/api/publications/{publicationId}", "/api/publications/{publicationId}",
"/api/publications", "/api/publications",
"/api/publications/latest",
"/error" "/error"
).permitAll() ).permitAll()
.requestMatchers( .requestMatchers(
POST, POST,
"/api/users/login", "/api/users/login",
"/api/users/refresh-token" "/api/users/refresh-token",
"/api/users"
).permitAll() ).permitAll()
.requestMatchers( .requestMatchers(
POST, POST,

View File

@@ -1,6 +1,7 @@
package org.codiki.exposition.picture; package org.codiki.exposition.picture;
import java.io.File; import java.io.File;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE; import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
@@ -8,6 +9,7 @@ import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
import org.codiki.application.picture.PictureUseCases; import org.codiki.application.picture.PictureUseCases;
import org.codiki.domain.picture.exception.PictureNotFoundException; import org.codiki.domain.picture.exception.PictureNotFoundException;
import org.codiki.domain.picture.model.Picture; import org.codiki.domain.picture.model.Picture;
import org.codiki.exposition.picture.model.PictureDto;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@@ -44,4 +46,12 @@ public class PictureController {
.orElseThrow(() -> new PictureNotFoundException(pictureId)); .orElseThrow(() -> new PictureNotFoundException(pictureId));
return new FileSystemResource(picture.contentFile()); return new FileSystemResource(picture.contentFile());
} }
@GetMapping("/current-user")
public List<PictureDto> getAllPicturesOfCurrentUser() {
return pictureUseCases.getAllOfCurrentUser()
.stream()
.map(PictureDto::new)
.toList();
}
} }

View File

@@ -0,0 +1,15 @@
package org.codiki.exposition.picture.model;
import org.codiki.domain.picture.model.Picture;
import java.time.ZonedDateTime;
import java.util.UUID;
public record PictureDto(
UUID id,
ZonedDateTime publishedAt
) {
public PictureDto(Picture picture) {
this(picture.id(), picture.publishedAt());
}
}

View File

@@ -1,28 +1,22 @@
package org.codiki.exposition.publication; package org.codiki.exposition.publication;
import org.codiki.application.publication.PublicationUseCases;
import org.codiki.domain.publication.exception.NoPublicationSearchResultException;
import org.codiki.domain.publication.exception.PublicationNotFoundException;
import org.codiki.domain.publication.model.Publication;
import org.codiki.domain.publication.model.PublicationEditionRequest;
import org.codiki.exposition.publication.model.PreviewContentRequest;
import org.codiki.exposition.publication.model.PreviewContentResponse;
import org.codiki.exposition.publication.model.PublicationDto;
import org.codiki.exposition.publication.model.PublicationEditionRequestDto;
import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.springframework.http.HttpStatus.CREATED; import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.NO_CONTENT; import static org.springframework.http.HttpStatus.NO_CONTENT;
import static org.springframework.util.ObjectUtils.isEmpty; import static org.springframework.util.ObjectUtils.isEmpty;
import org.codiki.application.publication.PublicationUseCases;
import org.codiki.domain.publication.exception.NoPublicationSearchResultException;
import org.codiki.domain.publication.exception.PublicationNotFoundException;
import org.codiki.domain.publication.model.Publication;
import org.codiki.domain.publication.model.PublicationEditionRequest;
import org.codiki.exposition.publication.model.PublicationDto;
import org.codiki.exposition.publication.model.PublicationEditionRequestDto;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@RequestMapping("/api/publications") @RequestMapping("/api/publications")
@@ -66,7 +60,7 @@ public class PublicationController {
@GetMapping @GetMapping
public List<PublicationDto> searchPublications(@RequestParam("query") String searchQuery) { public List<PublicationDto> searchPublications(@RequestParam("query") String searchQuery) {
final List<PublicationDto> publications = publicationUseCases.searchPublications(searchQuery) List<PublicationDto> publications = publicationUseCases.searchPublications(searchQuery)
.stream() .stream()
.map(PublicationDto::new) .map(PublicationDto::new)
.toList(); .toList();
@@ -77,4 +71,24 @@ public class PublicationController {
return publications; return publications;
} }
@GetMapping("/latest")
public List<PublicationDto> getLatestPublications() {
List<PublicationDto> publications = publicationUseCases.getLatest()
.stream()
.map(PublicationDto::new)
.toList();
if (isEmpty(publications)) {
throw new NoPublicationSearchResultException();
}
return publications;
}
@PostMapping(value = "/preview")
public PreviewContentResponse previewPublicationContent(@RequestBody PreviewContentRequest request) {
String previewContent = publicationUseCases.previewContent(request.text());
return new PreviewContentResponse(previewContent);
}
} }

View File

@@ -13,7 +13,7 @@ public record AuthorDto(
this( this(
author.id(), author.id(),
author.name(), author.name(),
author.image() author.photoId()
); );
} }
} }

View File

@@ -0,0 +1,5 @@
package org.codiki.exposition.publication.model;
public record PreviewContentRequest(
String text
) {}

View File

@@ -0,0 +1,5 @@
package org.codiki.exposition.publication.model;
public record PreviewContentResponse(
String text
) {}

View File

@@ -1,16 +1,16 @@
package org.codiki.exposition.publication.model; package org.codiki.exposition.publication.model;
import org.codiki.domain.publication.model.Publication;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.UUID; import java.util.UUID;
import org.codiki.domain.publication.model.Publication;
import org.codiki.exposition.category.model.CategoryDto;
public record PublicationDto( public record PublicationDto(
UUID id, UUID id,
String key, String key,
String title, String title,
String text, String text,
String parsedText,
String description, String description,
ZonedDateTime creationDate, ZonedDateTime creationDate,
UUID illustrationId, UUID illustrationId,
@@ -23,6 +23,7 @@ public record PublicationDto(
publication.key(), publication.key(),
publication.title(), publication.title(),
publication.text(), publication.text(),
publication.parsedText(),
publication.description(), publication.description(),
publication.creationDate(), publication.creationDate(),
publication.illustrationId(), publication.illustrationId(),

View File

@@ -0,0 +1,75 @@
package org.codiki.exposition.traffic;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.codiki.application.traffic.TrafficTraceUseCases;
import org.codiki.application.user.UserUseCases;
import org.codiki.domain.traffic.model.HttpMethod;
import org.codiki.domain.traffic.model.TrafficEndpoint;
import org.codiki.domain.user.model.User;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Optional;
import java.util.UUID;
@Component
@Aspect
public class ApiCallsLoggerAspect {
private static final String HTTP_HEADER_CORRELATION_ID = "x-correlation-id";
private final TrafficTraceUseCases trafficTraceUseCases;
private final UserUseCases userUseCases;
public ApiCallsLoggerAspect(
TrafficTraceUseCases trafficTraceUseCases,
UserUseCases userUseCases
) {
this.trafficTraceUseCases = trafficTraceUseCases;
this.userUseCases = userUseCases;
}
@Before("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void logGetApiCall(JoinPoint joinPoint) {
logApiCall();
}
@Before("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void logPostApiCall(JoinPoint joinPoint) {
logApiCall();
}
@Before("@annotation(org.springframework.web.bind.annotation.PutMapping)")
public void logPutApiCall(JoinPoint joinPoint) {
logApiCall();
}
@Before("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
public void logDeleteApiCall(JoinPoint joinPoint) {
logApiCall();
}
private void logApiCall() {
getHttpServletRequest().ifPresent(request ->
Optional.of(request.getMethod())
.flatMap(HttpMethod::fromString)
.ifPresent(queryHttpMethod -> {
String queryUriPath = request.getRequestURI();
TrafficEndpoint endpoint = new TrafficEndpoint(queryHttpMethod, queryUriPath);
UUID userId = userUseCases.getAuthenticatedUser()
.map(User::id)
.orElse(null);
String correlationId = request.getHeader(HTTP_HEADER_CORRELATION_ID);
trafficTraceUseCases.saveNewTrace(endpoint, userId, correlationId);
})
);
}
private static Optional<HttpServletRequest> getHttpServletRequest() {
return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.filter(ServletRequestAttributes.class::isInstance)
.map(ServletRequestAttributes.class::cast)
.map(ServletRequestAttributes::getRequest);
}
}

Some files were not shown because too many files have changed in this diff Show More