Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90701dcdce | |||
| 8439782430 | |||
| e6a7af840a | |||
| 568a2c6831 | |||
| c39a20cf96 | |||
| bf56314473 | |||
| 11caff1634 | |||
| b1478fd154 | |||
| dd174a6327 | |||
| 941380ad16 | |||
| 4f395d254d | |||
| 6a43acfd46 | |||
| 78b84dc62a | |||
| 1120bc9350 | |||
| a37254452b | |||
| cd9fdd28e8 | |||
| c3fc6a4e88 | |||
| cb58a4fbe0 | |||
| 64059984d3 | |||
| fdc8d34f82 | |||
| 95ec674eb8 | |||
| ea7be84ec3 | |||
| c709f2b44d | |||
| 7995a0b3e0 | |||
| bfd562bc60 | |||
| 4fb85a52e4 | |||
| e64352f54c | |||
| c2c03e415e | |||
| 0260bea951 | |||
| 8b8dbd6fe5 | |||
| 536c6e7b79 | |||
| e5a2b8993f | |||
| 38750a588c | |||
| ee026ec829 | |||
| 29b024d360 | |||
| cd12d1561a | |||
| c2eaf3d0cc | |||
| 4c9ac8944e | |||
| 4ff97044f0 | |||
| ead1932d48 | |||
| 4a7dcec363 | |||
| cb76a3253d | |||
| 681fd635b3 | |||
| 9467db2382 | |||
| 7ed3494808 | |||
| ceb310bf02 | |||
| 2c3106c5c1 | |||
| 4effa8231a | |||
| b78420e106 | |||
| dd08763161 | |||
| e0b1514965 | |||
| 69c91ec86a | |||
| 1bc45461c3 | |||
| 0dfb2a7e03 | |||
| a7c8e63b11 | |||
| ad97ba029e | |||
| 31f538c7f5 | |||
| c7cf71441f | |||
| b015f3a97e | |||
| b8e9d4e96e | |||
| c5f9a1d6e0 | |||
| 1cb6c731d8 | |||
| f6e33ed3b4 | |||
| 815770303c | |||
| d6ab257473 | |||
| cbc3a017e8 | |||
| d70663b898 | |||
| 4ffa565626 | |||
| ea732325e5 | |||
| f73a9d0b96 | |||
| 5573dd45d6 | |||
| eeae982a71 | |||
| bad5322abd | |||
| 39fe7a5ab6 | |||
| c800d22ccf | |||
| 2ac02688ab | |||
| 7d833e48e1 | |||
| 97ea331c51 | |||
| 4c4ca2dd98 | |||
| 204f2fb002 | |||
| 87a6fdc94a | |||
| fa21c85a13 | |||
| c367d5b613 | |||
| 2c967ebd8c | |||
| 381d935875 | |||
| d575773f73 | |||
| 36600bb1f4 | |||
| b27fd29230 | |||
| c02f7c039a | |||
| 8ba89d3e05 | |||
| 372652d332 | |||
| f12947acbd | |||
| 7305fb47c7 | |||
| a440199006 | |||
| 1432fbb395 | |||
| 08c804ccb5 | |||
| 68109f8666 | |||
| 315a01ea18 | |||
| 12619f6550 | |||
| ab3766b8b8 | |||
| 3861fb6b97 | |||
| 88b6eb56ae | |||
| 8ccd7f6058 | |||
| a98d6e8e64 | |||
| ab7cfd8147 | |||
| 5d9ca85b22 | |||
| 15de81394c | |||
| 4e2fe463e0 | |||
| 0e72547e95 | |||
| 845ca2acb8 | |||
| 25e29afcbb | |||
| 5295e32d86 | |||
| 8021814c31 | |||
| b5beca8661 | |||
| 56e742e39f | |||
| 48897c8b90 | |||
| 9d9ec013f5 | |||
| 01ba7cbd7d | |||
| 4984e488ae | |||
| 662d6c706b | |||
| 00dafe1da9 | |||
| a9bbfcf82c | |||
| 4c38512038 | |||
| fb49d45677 | |||
| 90f6709885 | |||
| b90df61020 | |||
| 1b79635ffa | |||
| 934820274b | |||
| 24ac5cf4fc | |||
| 29e445ff41 | |||
| e65a4e10d6 | |||
| 24aabd494e | |||
| b41b2103f0 | |||
| a11450cbcf | |||
| d419b4c72a | |||
| 9216696b1a | |||
| cdfe1d14ef | |||
| fda355a690 | |||
| fc883373d0 | |||
| 50020c2f91 | |||
| 44b463d9d5 |
@@ -1,9 +1,6 @@
|
|||||||
|
# mariadb
|
||||||
MYSQL_ROOT_PASSWORD=
|
MYSQL_ROOT_PASSWORD=
|
||||||
MYSQL_HOST=db
|
|
||||||
MYSQL_DATABASE=
|
|
||||||
MYSQL_USER=
|
|
||||||
MYSQL_PASSWORD=
|
MYSQL_PASSWORD=
|
||||||
|
# simplenotes
|
||||||
|
DB_PASSWORD=
|
||||||
JWT_SECRET=
|
JWT_SECRET=
|
||||||
JWT_REFRESH_SECRET=
|
|
||||||
CORS=false
|
|
||||||
PORT=8081
|
|
||||||
|
|||||||
@@ -1,24 +1,6 @@
|
|||||||
# Java
|
# Gradle
|
||||||
.mtj.tmp/
|
build/
|
||||||
*.class
|
.gradle
|
||||||
*.jar
|
|
||||||
*.war
|
|
||||||
*.ear
|
|
||||||
*.nar
|
|
||||||
hs_err_pid*
|
|
||||||
|
|
||||||
# Maven
|
|
||||||
target/
|
|
||||||
pom.xml.tag
|
|
||||||
pom.xml.releaseBackup
|
|
||||||
pom.xml.versionsBackup
|
|
||||||
pom.xml.next
|
|
||||||
pom.xml.bak
|
|
||||||
release.properties
|
|
||||||
dependency-reduced-pom.xml
|
|
||||||
buildNumber.properties
|
|
||||||
.mvn/timing.properties
|
|
||||||
.mvn/wrapper/maven-wrapper.jar
|
|
||||||
|
|
||||||
# IntelliJ
|
# IntelliJ
|
||||||
out/
|
out/
|
||||||
@@ -28,11 +10,8 @@ out/
|
|||||||
*.ipr
|
*.ipr
|
||||||
*.iws
|
*.iws
|
||||||
|
|
||||||
# Vue
|
|
||||||
node_modules
|
|
||||||
/dist
|
|
||||||
|
|
||||||
# Local env files
|
# Local env files
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
@@ -49,77 +28,19 @@ pids
|
|||||||
*.seed
|
*.seed
|
||||||
*.pid.lock
|
*.pid.lock
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
# Dependency directories
|
||||||
node_modules/
|
node_modules/
|
||||||
jspm_packages/
|
jspm_packages/
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
|
||||||
typings/
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
# Yarn Integrity file
|
||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
|
|
||||||
# dotenv environment variables file
|
# h2 db
|
||||||
.env
|
*.db
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
# lucene index
|
||||||
.cache
|
.lucene/
|
||||||
|
|
||||||
# next.js build output
|
# python
|
||||||
.next
|
__pycache__
|
||||||
|
|
||||||
# nuxt.js build output
|
|
||||||
.nuxt
|
|
||||||
|
|
||||||
# Nuxt generate
|
|
||||||
dist
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless
|
|
||||||
|
|
||||||
# IDE / Editor
|
|
||||||
.idea
|
|
||||||
|
|
||||||
# Service worker
|
|
||||||
sw.*
|
|
||||||
*.private.env.json
|
|
||||||
|
|
||||||
# Certificates
|
|
||||||
data/
|
|
||||||
letsencrypt/
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Enable auto-env through the sdkman_auto_env config
|
||||||
|
# Add key=value pairs of SDKs to use below
|
||||||
|
java=14.0.2-open
|
||||||
|
gradle=6.7
|
||||||
|
kotlin=1.4.10
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
(security) {
|
|
||||||
header * {
|
|
||||||
-Server
|
|
||||||
-Date
|
|
||||||
|
|
||||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
||||||
Feature-Policy "geolocation none; midi none; notifications none; push none; sync-xhr none; microphone none; camera none; magnetometer none; gyroscope none; speaker self; vibrate none; fullscreen self; payment none"
|
|
||||||
X-Content-Type-Options "nosniff"
|
|
||||||
X-Frame-Options "DENY"
|
|
||||||
X-XSS-Protection "1; mode=block"
|
|
||||||
Referrer-Policy "no-referrer-when-downgrade"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(common) {
|
|
||||||
@ignore {
|
|
||||||
path *.php
|
|
||||||
}
|
|
||||||
respond @ignore "no" 404
|
|
||||||
|
|
||||||
try_files {path} {path}/ {path}/index.html
|
|
||||||
|
|
||||||
encode gzip
|
|
||||||
}
|
|
||||||
|
|
||||||
(nuxt) {
|
|
||||||
@nuxt {
|
|
||||||
path /_nuxt/*
|
|
||||||
}
|
|
||||||
|
|
||||||
header @nuxt Cache-Control "public, max-age=31536000" # 1 year
|
|
||||||
}
|
|
||||||
|
|
||||||
simplenotes.be {
|
|
||||||
import security
|
|
||||||
import nuxt
|
|
||||||
import common
|
|
||||||
|
|
||||||
@404 {
|
|
||||||
expression {http.error.status_code} == 404
|
|
||||||
}
|
|
||||||
|
|
||||||
handle_errors {
|
|
||||||
rewrite @404 /404.html
|
|
||||||
file_server
|
|
||||||
import security
|
|
||||||
}
|
|
||||||
|
|
||||||
route /* {
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
route /api/* {
|
|
||||||
uri strip_prefix /api
|
|
||||||
reverse_proxy http://localhost:8081
|
|
||||||
}
|
|
||||||
|
|
||||||
header Content-Security-Policy "default-src 'self' 'unsafe-inline';"
|
|
||||||
|
|
||||||
root * /var/www/simplenotes.be
|
|
||||||
|
|
||||||
log {
|
|
||||||
output file /var/log/www/simplenotes.be.json
|
|
||||||
format json
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
www.simplenotes.be {
|
|
||||||
redir * https://simplenotes.be{path}
|
|
||||||
}
|
|
||||||
|
|
||||||
docs.simplenotes.be {
|
|
||||||
import security
|
|
||||||
import common
|
|
||||||
|
|
||||||
file_server
|
|
||||||
|
|
||||||
root * /var/www/docs.simplenotes.be
|
|
||||||
|
|
||||||
log {
|
|
||||||
output file /var/log/www/docs.simplenotes.be.json
|
|
||||||
format json
|
|
||||||
}
|
|
||||||
|
|
||||||
header Content-Security-Policy "default-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;"
|
|
||||||
}
|
|
||||||
|
|
||||||
portfolio.simplenotes.be {
|
|
||||||
import security
|
|
||||||
import common
|
|
||||||
import nuxt
|
|
||||||
|
|
||||||
file_server
|
|
||||||
root * /var/www/portfolio.simplenotes.be
|
|
||||||
|
|
||||||
log {
|
|
||||||
output file /var/log/www/portfolio.simplenotes.be.json
|
|
||||||
format json
|
|
||||||
}
|
|
||||||
|
|
||||||
# header Content-Security-Policy "default-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;"
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
FROM openjdk:14-alpine as jdkbuilder
|
||||||
|
|
||||||
|
RUN apk add --no-cache binutils
|
||||||
|
|
||||||
|
ENV MODULES java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.scripting,java.security.jgss,java.sql,java.sql.rowset,java.transaction.xa,java.xml,jdk.net
|
||||||
|
|
||||||
|
RUN jlink --output /myjdk --module-path $JAVA_HOME/jmods --add-modules $MODULES --no-header-files --no-man-pages --strip-debug --compress=2
|
||||||
|
|
||||||
|
RUN strip -p --strip-unneeded /myjdk/lib/server/libjvm.so
|
||||||
|
|
||||||
|
FROM alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
|
ENV APPLICATION_USER simplenotes
|
||||||
|
RUN adduser -D -g '' $APPLICATION_USER
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
RUN chown -R $APPLICATION_USER /app
|
||||||
|
|
||||||
|
USER $APPLICATION_USER
|
||||||
|
|
||||||
|
COPY --from=jdkbuilder /myjdk /myjdk
|
||||||
|
COPY app/build/libs/app-with-dependencies*.jar /app/simplenotes.jar
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
CMD [ \
|
||||||
|
"/myjdk/bin/java", \
|
||||||
|
"--add-opens", \
|
||||||
|
"java.base/java.nio=ALL-UNNAMED", \
|
||||||
|
"-server", \
|
||||||
|
"-XX:+UnlockExperimentalVMOptions", \
|
||||||
|
"-Xms64m", \
|
||||||
|
"-Xmx256m", \
|
||||||
|
"-XX:+UseG1GC", \
|
||||||
|
"-XX:MaxGCPauseMillis=100", \
|
||||||
|
"-XX:+UseStringDeduplication", \
|
||||||
|
"-jar", \
|
||||||
|
"simplenotes.jar" \
|
||||||
|
]
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
FROM maven:3.6.3-jdk-14 as builder
|
|
||||||
|
|
||||||
WORKDIR /tmp
|
|
||||||
|
|
||||||
# Cache dependencies
|
|
||||||
COPY api/pom.xml .
|
|
||||||
RUN mvn verify clean --fail-never
|
|
||||||
|
|
||||||
COPY api/resources resources
|
|
||||||
COPY api/src src
|
|
||||||
COPY api/test test
|
|
||||||
|
|
||||||
RUN mvn package -DskipTests
|
|
||||||
|
|
||||||
FROM openjdk:14-alpine
|
|
||||||
|
|
||||||
ENV APPLICATION_USER ktor
|
|
||||||
RUN adduser -D -g '' $APPLICATION_USER
|
|
||||||
|
|
||||||
RUN mkdir /app
|
|
||||||
RUN chown -R $APPLICATION_USER /app
|
|
||||||
|
|
||||||
USER $APPLICATION_USER
|
|
||||||
|
|
||||||
COPY --from=builder /tmp/target/api-*-jar-with-dependencies.jar /app/notes-api.jar
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
CMD ["java", "-server", "-XX:+UnlockExperimentalVMOptions", "-XX:InitialRAMFraction=2", "-XX:MinRAMFraction=2", "-XX:MaxRAMFraction=2", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "notes-api.jar"]
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Hubert Van De Walle
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# SimpleNotes, a simple markdown note taking website
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Docker
|
||||||
|
- docker-compose
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
- Copy the docker-compose.yml somewhere
|
||||||
|
- In the same directory, copy the *.env.dist* file and rename it to *.env*
|
||||||
|
- Edit the variables inside *.env* (see below)
|
||||||
|
- Run it with `docker-compose up -d`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The app is configured with environments variables.
|
||||||
|
If no match is found within the env, a default value is read from a yaml file in simplenotes-app/src/main/resources/application.yaml.
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
FORMAT: 1A
|
|
||||||
HOST: http://localhost:5000
|
|
||||||
|
|
||||||
# Notes API
|
|
||||||
|
|
||||||
<!-- include(./users/index.apib) -->
|
|
||||||
<!-- include(./notes/index.apib) -->
|
|
||||||
<!-- include(./tags/index.apib) -->
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
# Data Structures
|
|
||||||
|
|
||||||
## Chapter (object)
|
|
||||||
+ title: Chapter 1 (string)
|
|
||||||
+ content: ... (string)
|
|
||||||
|
|
||||||
|
|
||||||
# Group Notes
|
|
||||||
|
|
||||||
## Notes [/notes]
|
|
||||||
|
|
||||||
### Create a Note [POST]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
|
|
||||||
+ Attributes (object)
|
|
||||||
+ title: `This is a title` (string)
|
|
||||||
+ tags: Dev, Server (array[string])
|
|
||||||
+ chapters (array)
|
|
||||||
+ (object)
|
|
||||||
+ title: `Chapter 1` (string)
|
|
||||||
+ content: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.` (string)
|
|
||||||
|
|
||||||
+ Response 201 (application/json)
|
|
||||||
+ Attributes (object)
|
|
||||||
+ uuid: `107c90ae-a41e-4c8e-b5e3-1a269cfe044b` (string)
|
|
||||||
|
|
||||||
### Get Notes [GET]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
|
|
||||||
|
|
||||||
+ Response 200 (application/json)
|
|
||||||
+ Attributes (array)
|
|
||||||
+ (object)
|
|
||||||
+ uuid: `123e4567-e89b-12d3-a456-426614174000` (string)
|
|
||||||
+ title: Kotlin (string)
|
|
||||||
+ tags: Dev, Server (array[string])
|
|
||||||
+ updatedAt: `2020-01-20T00:00:00` (string)
|
|
||||||
+ (object)
|
|
||||||
+ uuid: `107c90ae-a41e-4c8e-b5e3-1a269cfe044b` (string)
|
|
||||||
+ title: Java (string)
|
|
||||||
+ tags: Dev (array[string])
|
|
||||||
+ updatedAt: `2018-01-20T00:00:00` (string)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Note [/notes/{noteUuid}]
|
|
||||||
|
|
||||||
+ Parameters
|
|
||||||
+ noteUuid: `123e4567-e89b-12d3-a456-426614174000` (string) - The note UUID.
|
|
||||||
|
|
||||||
|
|
||||||
### Get a Note [GET]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
|
|
||||||
+ Response 200 (application/json)
|
|
||||||
+ Attributes (object)
|
|
||||||
+ uuid: `123e4567-e89b-12d3-a456-426614174000` (string)
|
|
||||||
+ title: `This is a title` (string)
|
|
||||||
+ updatedAt: `2020-05-08T11:56:01` (string)
|
|
||||||
+ tags: Dev, Server (array[string])
|
|
||||||
+ chapters (array)
|
|
||||||
+ (Chapter)
|
|
||||||
+ title: Introduction
|
|
||||||
+ content: ...
|
|
||||||
+ (Chapter)
|
|
||||||
+ title: Objects
|
|
||||||
+ content: ...
|
|
||||||
|
|
||||||
+ Response 404
|
|
||||||
|
|
||||||
|
|
||||||
### Update a Note [PATCH]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
|
|
||||||
+ Attributes (object)
|
|
||||||
+ title: NewTitle (string)
|
|
||||||
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
|
|
||||||
+ Attributes (object)
|
|
||||||
+ tags: new, tags (array[string])
|
|
||||||
|
|
||||||
|
|
||||||
+ Response 200
|
|
||||||
|
|
||||||
+ Response 404
|
|
||||||
|
|
||||||
### Delete a Note [DELETE]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
|
|
||||||
+ Response 200
|
|
||||||
|
|
||||||
+ Response 404
|
|
||||||
|
|
||||||
|
|
||||||
## Chapters [/notes/{noteTitle}/chapters/{chapterNumber}]
|
|
||||||
|
|
||||||
+ Parameters
|
|
||||||
+ noteTitle: `Kotlin` (string) - The title of the Note.
|
|
||||||
+ chapterNumber: `Kotlin` (number) - The chapter number.
|
|
||||||
|
|
||||||
### Post a chapter [POST]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
+ Attributes (Chapter)
|
|
||||||
+ title: Chapter 1 (string)
|
|
||||||
+ content: ... (string)
|
|
||||||
|
|
||||||
+ Response 201
|
|
||||||
|
|
||||||
+ Response 404
|
|
||||||
|
|
||||||
### Patch a chapter [PATCH]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
|
|
||||||
+ Attributes (object)
|
|
||||||
+ title: new title (string)
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
|
|
||||||
+ Attributes (object)
|
|
||||||
+ content: ... (string)
|
|
||||||
|
|
||||||
+ Response 200
|
|
||||||
|
|
||||||
+ Response 404
|
|
||||||
|
|
||||||
|
|
||||||
### Delete a chapter [DELETE]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
|
|
||||||
+ Response 200
|
|
||||||
|
|
||||||
+ Response 404
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Group Tags
|
|
||||||
|
|
||||||
## Tags [/tags]
|
|
||||||
|
|
||||||
### Get all tags [GET]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
|
|
||||||
|
|
||||||
+ Response 200 (application/json)
|
|
||||||
+ Attributes
|
|
||||||
+ tags: Dev, Server (array[string])
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# Data Structures
|
|
||||||
|
|
||||||
## Login (object)
|
|
||||||
+ username: babar (string)
|
|
||||||
+ password: tortue (string)
|
|
||||||
|
|
||||||
|
|
||||||
## InvalidCredentials (object)
|
|
||||||
+ description: Invalid credentials (string),
|
|
||||||
+ error: Bad Request (string),
|
|
||||||
+ status_code: 401 (number)
|
|
||||||
|
|
||||||
# Group Accounts
|
|
||||||
|
|
||||||
## Account [/user]
|
|
||||||
|
|
||||||
### Register a new user [POST]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Attributes (object)
|
|
||||||
+ username: babar (string)
|
|
||||||
+ password: tortue (string)
|
|
||||||
|
|
||||||
+ Response 200 (application/json)
|
|
||||||
+ Attributes (object)
|
|
||||||
+ message: Created (string)
|
|
||||||
|
|
||||||
+ Response 409 (application/json)
|
|
||||||
+ Attributes (object)
|
|
||||||
+ message: User already exists (string)
|
|
||||||
|
|
||||||
### Delete a user [DELETE]
|
|
||||||
|
|
||||||
+ Request
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
|
|
||||||
+ Response 200 (application/json)
|
|
||||||
|
|
||||||
|
|
||||||
## Authentication [/user/login]
|
|
||||||
Authenticate one user to access protected routing.
|
|
||||||
|
|
||||||
### Authenticate a user [POST]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Attributes (Login)
|
|
||||||
|
|
||||||
+ Response 200 (application/json)
|
|
||||||
+ Attributes
|
|
||||||
+ token: <token>
|
|
||||||
+ refreshToken: `<refresh-token>`
|
|
||||||
|
|
||||||
+ Response 401 (application/json)
|
|
||||||
+ Attributes (InvalidCredentials)
|
|
||||||
|
|
||||||
## Token refresh [/user/refresh_token]
|
|
||||||
|
|
||||||
### Refresh JWT token [POST]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Attributes
|
|
||||||
+ refreshToken: `<refresh-token>`
|
|
||||||
|
|
||||||
+ Response 200 (application/json)
|
|
||||||
+ Attributes
|
|
||||||
+ token: <token>
|
|
||||||
+ refreshToken: `<refresh-token>`
|
|
||||||
|
|
||||||
+ Response 401 (application/json)
|
|
||||||
+ Attributes (InvalidCredentials)
|
|
||||||
|
|
||||||
## User Info [/user/me]
|
|
||||||
Receive the username and email from the currently logged in user
|
|
||||||
|
|
||||||
### Get User Info [GET]
|
|
||||||
|
|
||||||
+ Request (application/json)
|
|
||||||
+ Headers
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
|
|
||||||
|
|
||||||
+ Response 200 (application/json)
|
|
||||||
+ Attributes
|
|
||||||
+ user: (object)
|
|
||||||
+ username: babar (string)
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
/*
|
|
||||||
Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
or more contributor license agreements. See the NOTICE file
|
|
||||||
distributed with this work for additional information
|
|
||||||
regarding copyright ownership. The ASF licenses this file
|
|
||||||
to you under the Apache License, Version 2.0 (the
|
|
||||||
"License"); you may not use this file except in compliance
|
|
||||||
with the License. You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing,
|
|
||||||
software distributed under the License is distributed on an
|
|
||||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
KIND, either express or implied. See the License for the
|
|
||||||
specific language governing permissions and limitations
|
|
||||||
under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import java.net.*;
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.channels.*;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
public class MavenWrapperDownloader {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
|
|
||||||
*/
|
|
||||||
private static final String DEFAULT_DOWNLOAD_URL =
|
|
||||||
"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
|
|
||||||
* use instead of the default one.
|
|
||||||
*/
|
|
||||||
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
|
|
||||||
".mvn/wrapper/maven-wrapper.properties";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Path where the maven-wrapper.jar will be saved to.
|
|
||||||
*/
|
|
||||||
private static final String MAVEN_WRAPPER_JAR_PATH =
|
|
||||||
".mvn/wrapper/maven-wrapper.jar";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the property which should be used to override the default download url for the wrapper.
|
|
||||||
*/
|
|
||||||
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
|
|
||||||
|
|
||||||
public static void main(String args[]) {
|
|
||||||
System.out.println("- Downloader started");
|
|
||||||
File baseDirectory = new File(args[0]);
|
|
||||||
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
|
|
||||||
|
|
||||||
// If the maven-wrapper.properties exists, read it and check if it contains a custom
|
|
||||||
// wrapperUrl parameter.
|
|
||||||
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
|
|
||||||
String url = DEFAULT_DOWNLOAD_URL;
|
|
||||||
if(mavenWrapperPropertyFile.exists()) {
|
|
||||||
FileInputStream mavenWrapperPropertyFileInputStream = null;
|
|
||||||
try {
|
|
||||||
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
|
|
||||||
Properties mavenWrapperProperties = new Properties();
|
|
||||||
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
|
|
||||||
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
|
|
||||||
} catch (IOException e) {
|
|
||||||
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if(mavenWrapperPropertyFileInputStream != null) {
|
|
||||||
mavenWrapperPropertyFileInputStream.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Ignore ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
System.out.println("- Downloading from: : " + url);
|
|
||||||
|
|
||||||
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
|
|
||||||
if(!outputFile.getParentFile().exists()) {
|
|
||||||
if(!outputFile.getParentFile().mkdirs()) {
|
|
||||||
System.out.println(
|
|
||||||
"- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
|
|
||||||
try {
|
|
||||||
downloadFileFromURL(url, outputFile);
|
|
||||||
System.out.println("Done");
|
|
||||||
System.exit(0);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
System.out.println("- Error downloading");
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
|
|
||||||
URL website = new URL(urlString);
|
|
||||||
ReadableByteChannel rbc;
|
|
||||||
rbc = Channels.newChannel(website.openStream());
|
|
||||||
FileOutputStream fos = new FileOutputStream(destination);
|
|
||||||
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
|
||||||
fos.close();
|
|
||||||
rbc.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Maven2 Start Up Batch script
|
|
||||||
#
|
|
||||||
# Required ENV vars:
|
|
||||||
# ------------------
|
|
||||||
# JAVA_HOME - location of a JDK home dir
|
|
||||||
#
|
|
||||||
# Optional ENV vars
|
|
||||||
# -----------------
|
|
||||||
# M2_HOME - location of maven2's installed home dir
|
|
||||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
|
||||||
# e.g. to debug Maven itself, use
|
|
||||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
|
||||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
|
||||||
|
|
||||||
if [ -f /etc/mavenrc ] ; then
|
|
||||||
. /etc/mavenrc
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "$HOME/.mavenrc" ] ; then
|
|
||||||
. "$HOME/.mavenrc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
fi
|
|
||||||
|
|
||||||
# OS specific support. $var _must_ be set to either true or false.
|
|
||||||
cygwin=false;
|
|
||||||
darwin=false;
|
|
||||||
mingw=false
|
|
||||||
case "`uname`" in
|
|
||||||
CYGWIN*) cygwin=true ;;
|
|
||||||
MINGW*) mingw=true;;
|
|
||||||
Darwin*) darwin=true
|
|
||||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
|
||||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
|
||||||
if [ -z "$JAVA_HOME" ]; then
|
|
||||||
if [ -x "/usr/libexec/java_home" ]; then
|
|
||||||
export JAVA_HOME="`/usr/libexec/java_home`"
|
|
||||||
else
|
|
||||||
export JAVA_HOME="/Library/Java/Home"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ -z "$JAVA_HOME" ] ; then
|
|
||||||
if [ -r /etc/gentoo-release ] ; then
|
|
||||||
JAVA_HOME=`java-config --jre-home`
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$M2_HOME" ] ; then
|
|
||||||
## resolve links - $0 may be a link to maven's home
|
|
||||||
PRG="$0"
|
|
||||||
|
|
||||||
# need this for relative symlinks
|
|
||||||
while [ -h "$PRG" ] ; do
|
|
||||||
ls=`ls -ld "$PRG"`
|
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
|
||||||
PRG="$link"
|
|
||||||
else
|
|
||||||
PRG="`dirname "$PRG"`/$link"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
saveddir=`pwd`
|
|
||||||
|
|
||||||
M2_HOME=`dirname "$PRG"`/..
|
|
||||||
|
|
||||||
# make it fully qualified
|
|
||||||
M2_HOME=`cd "$M2_HOME" && pwd`
|
|
||||||
|
|
||||||
cd "$saveddir"
|
|
||||||
# echo Using m2 at $M2_HOME
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
|
||||||
if $cygwin ; then
|
|
||||||
[ -n "$M2_HOME" ] &&
|
|
||||||
M2_HOME=`cygpath --unix "$M2_HOME"`
|
|
||||||
[ -n "$JAVA_HOME" ] &&
|
|
||||||
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
|
||||||
[ -n "$CLASSPATH" ] &&
|
|
||||||
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
|
||||||
if $mingw ; then
|
|
||||||
[ -n "$M2_HOME" ] &&
|
|
||||||
M2_HOME="`(cd "$M2_HOME"; pwd)`"
|
|
||||||
[ -n "$JAVA_HOME" ] &&
|
|
||||||
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
|
|
||||||
# TODO classpath?
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$JAVA_HOME" ]; then
|
|
||||||
javaExecutable="`which javac`"
|
|
||||||
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
|
|
||||||
# readlink(1) is not available as standard on Solaris 10.
|
|
||||||
readLink=`which readlink`
|
|
||||||
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
|
|
||||||
if $darwin ; then
|
|
||||||
javaHome="`dirname \"$javaExecutable\"`"
|
|
||||||
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
|
|
||||||
else
|
|
||||||
javaExecutable="`readlink -f \"$javaExecutable\"`"
|
|
||||||
fi
|
|
||||||
javaHome="`dirname \"$javaExecutable\"`"
|
|
||||||
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
|
|
||||||
JAVA_HOME="$javaHome"
|
|
||||||
export JAVA_HOME
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$JAVACMD" ] ; then
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
|
||||||
else
|
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD="`which java`"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
|
||||||
echo " We cannot execute $JAVACMD" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$JAVA_HOME" ] ; then
|
|
||||||
echo "Warning: JAVA_HOME environment variable is not set."
|
|
||||||
fi
|
|
||||||
|
|
||||||
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
|
|
||||||
|
|
||||||
# traverses directory structure from process work directory to filesystem root
|
|
||||||
# first directory with .mvn subdirectory is considered project base directory
|
|
||||||
find_maven_basedir() {
|
|
||||||
|
|
||||||
if [ -z "$1" ]
|
|
||||||
then
|
|
||||||
echo "Path not specified to find_maven_basedir"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
basedir="$1"
|
|
||||||
wdir="$1"
|
|
||||||
while [ "$wdir" != '/' ] ; do
|
|
||||||
if [ -d "$wdir"/.mvn ] ; then
|
|
||||||
basedir=$wdir
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
|
||||||
if [ -d "${wdir}" ]; then
|
|
||||||
wdir=`cd "$wdir/.."; pwd`
|
|
||||||
fi
|
|
||||||
# end of workaround
|
|
||||||
done
|
|
||||||
echo "${basedir}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# concatenates all lines of a file
|
|
||||||
concat_lines() {
|
|
||||||
if [ -f "$1" ]; then
|
|
||||||
echo "$(tr -s '\n' ' ' < "$1")"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
BASE_DIR=`find_maven_basedir "$(pwd)"`
|
|
||||||
if [ -z "$BASE_DIR" ]; then
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
|
|
||||||
##########################################################################################
|
|
||||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
|
||||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
|
||||||
##########################################################################################
|
|
||||||
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
|
|
||||||
if [ "$MVNW_VERBOSE" = true ]; then
|
|
||||||
echo "Found .mvn/wrapper/maven-wrapper.jar"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [ "$MVNW_VERBOSE" = true ]; then
|
|
||||||
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
|
|
||||||
fi
|
|
||||||
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar"
|
|
||||||
while IFS="=" read key value; do
|
|
||||||
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
|
|
||||||
esac
|
|
||||||
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
|
|
||||||
if [ "$MVNW_VERBOSE" = true ]; then
|
|
||||||
echo "Downloading from: $jarUrl"
|
|
||||||
fi
|
|
||||||
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
|
|
||||||
|
|
||||||
if command -v wget > /dev/null; then
|
|
||||||
if [ "$MVNW_VERBOSE" = true ]; then
|
|
||||||
echo "Found wget ... using wget"
|
|
||||||
fi
|
|
||||||
wget "$jarUrl" -O "$wrapperJarPath"
|
|
||||||
elif command -v curl > /dev/null; then
|
|
||||||
if [ "$MVNW_VERBOSE" = true ]; then
|
|
||||||
echo "Found curl ... using curl"
|
|
||||||
fi
|
|
||||||
curl -o "$wrapperJarPath" "$jarUrl"
|
|
||||||
else
|
|
||||||
if [ "$MVNW_VERBOSE" = true ]; then
|
|
||||||
echo "Falling back to using Java to download"
|
|
||||||
fi
|
|
||||||
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
|
||||||
if [ -e "$javaClass" ]; then
|
|
||||||
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
|
||||||
if [ "$MVNW_VERBOSE" = true ]; then
|
|
||||||
echo " - Compiling MavenWrapperDownloader.java ..."
|
|
||||||
fi
|
|
||||||
# Compiling the Java class
|
|
||||||
("$JAVA_HOME/bin/javac" "$javaClass")
|
|
||||||
fi
|
|
||||||
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
|
||||||
# Running the downloader
|
|
||||||
if [ "$MVNW_VERBOSE" = true ]; then
|
|
||||||
echo " - Running MavenWrapperDownloader.java ..."
|
|
||||||
fi
|
|
||||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
##########################################################################################
|
|
||||||
# End of extension
|
|
||||||
##########################################################################################
|
|
||||||
|
|
||||||
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
|
|
||||||
if [ "$MVNW_VERBOSE" = true ]; then
|
|
||||||
echo $MAVEN_PROJECTBASEDIR
|
|
||||||
fi
|
|
||||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
|
||||||
|
|
||||||
# For Cygwin, switch paths to Windows format before running java
|
|
||||||
if $cygwin; then
|
|
||||||
[ -n "$M2_HOME" ] &&
|
|
||||||
M2_HOME=`cygpath --path --windows "$M2_HOME"`
|
|
||||||
[ -n "$JAVA_HOME" ] &&
|
|
||||||
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
|
|
||||||
[ -n "$CLASSPATH" ] &&
|
|
||||||
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
|
|
||||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
|
||||||
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
|
|
||||||
fi
|
|
||||||
|
|
||||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
|
||||||
|
|
||||||
exec "$JAVACMD" \
|
|
||||||
$MAVEN_OPTS \
|
|
||||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
|
||||||
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
|
||||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
@REM ----------------------------------------------------------------------------
|
|
||||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
@REM or more contributor license agreements. See the NOTICE file
|
|
||||||
@REM distributed with this work for additional information
|
|
||||||
@REM regarding copyright ownership. The ASF licenses this file
|
|
||||||
@REM to you under the Apache License, Version 2.0 (the
|
|
||||||
@REM "License"); you may not use this file except in compliance
|
|
||||||
@REM with the License. You may obtain a copy of the License at
|
|
||||||
@REM
|
|
||||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@REM
|
|
||||||
@REM Unless required by applicable law or agreed to in writing,
|
|
||||||
@REM software distributed under the License is distributed on an
|
|
||||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
@REM KIND, either express or implied. See the License for the
|
|
||||||
@REM specific language governing permissions and limitations
|
|
||||||
@REM under the License.
|
|
||||||
@REM ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@REM ----------------------------------------------------------------------------
|
|
||||||
@REM Maven2 Start Up Batch script
|
|
||||||
@REM
|
|
||||||
@REM Required ENV vars:
|
|
||||||
@REM JAVA_HOME - location of a JDK home dir
|
|
||||||
@REM
|
|
||||||
@REM Optional ENV vars
|
|
||||||
@REM M2_HOME - location of maven2's installed home dir
|
|
||||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
|
||||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
|
|
||||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
|
||||||
@REM e.g. to debug Maven itself, use
|
|
||||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
|
||||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
|
||||||
@REM ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
|
||||||
@echo off
|
|
||||||
@REM set title of command window
|
|
||||||
title %0
|
|
||||||
@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
|
|
||||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
|
||||||
|
|
||||||
@REM set %HOME% to equivalent of $HOME
|
|
||||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
|
||||||
|
|
||||||
@REM Execute a user defined script before this one
|
|
||||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
|
||||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
|
||||||
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
|
|
||||||
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
|
|
||||||
:skipRcPre
|
|
||||||
|
|
||||||
@setlocal
|
|
||||||
|
|
||||||
set ERROR_CODE=0
|
|
||||||
|
|
||||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
|
||||||
@setlocal
|
|
||||||
|
|
||||||
@REM ==== START VALIDATION ====
|
|
||||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Error: JAVA_HOME not found in your environment. >&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
|
||||||
echo location of your Java installation. >&2
|
|
||||||
echo.
|
|
||||||
goto error
|
|
||||||
|
|
||||||
:OkJHome
|
|
||||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
|
||||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
|
||||||
echo location of your Java installation. >&2
|
|
||||||
echo.
|
|
||||||
goto error
|
|
||||||
|
|
||||||
@REM ==== END VALIDATION ====
|
|
||||||
|
|
||||||
:init
|
|
||||||
|
|
||||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
|
||||||
@REM Fallback to current working directory if not found.
|
|
||||||
|
|
||||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
|
||||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
|
||||||
|
|
||||||
set EXEC_DIR=%CD%
|
|
||||||
set WDIR=%EXEC_DIR%
|
|
||||||
:findBaseDir
|
|
||||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
|
||||||
cd ..
|
|
||||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
|
||||||
set WDIR=%CD%
|
|
||||||
goto findBaseDir
|
|
||||||
|
|
||||||
:baseDirFound
|
|
||||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
|
||||||
cd "%EXEC_DIR%"
|
|
||||||
goto endDetectBaseDir
|
|
||||||
|
|
||||||
:baseDirNotFound
|
|
||||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
|
||||||
cd "%EXEC_DIR%"
|
|
||||||
|
|
||||||
:endDetectBaseDir
|
|
||||||
|
|
||||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
|
||||||
|
|
||||||
@setlocal EnableExtensions EnableDelayedExpansion
|
|
||||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
|
||||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
|
||||||
|
|
||||||
:endReadAdditionalConfig
|
|
||||||
|
|
||||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
|
||||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
|
||||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
|
||||||
|
|
||||||
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar"
|
|
||||||
FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO (
|
|
||||||
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
|
|
||||||
)
|
|
||||||
|
|
||||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
|
||||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
|
||||||
if exist %WRAPPER_JAR% (
|
|
||||||
echo Found %WRAPPER_JAR%
|
|
||||||
) else (
|
|
||||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
|
||||||
echo Downloading from: %DOWNLOAD_URL%
|
|
||||||
powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"
|
|
||||||
echo Finished downloading %WRAPPER_JAR%
|
|
||||||
)
|
|
||||||
@REM End of extension
|
|
||||||
|
|
||||||
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
|
||||||
if ERRORLEVEL 1 goto error
|
|
||||||
goto end
|
|
||||||
|
|
||||||
:error
|
|
||||||
set ERROR_CODE=1
|
|
||||||
|
|
||||||
:end
|
|
||||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
|
||||||
|
|
||||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
|
|
||||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
|
||||||
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
|
|
||||||
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
|
|
||||||
:skipRcPost
|
|
||||||
|
|
||||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
|
||||||
if "%MAVEN_BATCH_PAUSE%" == "on" pause
|
|
||||||
|
|
||||||
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
|
|
||||||
|
|
||||||
exit /B %ERROR_CODE%
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
<?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>be.vandewalleh</groupId>
|
|
||||||
<artifactId>api</artifactId>
|
|
||||||
<version>0.0.1</version>
|
|
||||||
<name>api</name>
|
|
||||||
<properties>
|
|
||||||
<!-- versions -->
|
|
||||||
<ktor_version>1.3.2</ktor_version>
|
|
||||||
<kotlin_version>1.3.70</kotlin_version>
|
|
||||||
<logback_version>1.2.1</logback_version>
|
|
||||||
<junit_version>5.6.2</junit_version>
|
|
||||||
<ktorm_version>3.0.0</ktorm_version>
|
|
||||||
<mariadb_version>2.6.0</mariadb_version>
|
|
||||||
<kodein_version>7.0.0</kodein_version>
|
|
||||||
<flyway_version>6.3.3</flyway_version>
|
|
||||||
<javajwt_version>3.10.2</javajwt_version>
|
|
||||||
<jbcrypt_version>0.4</jbcrypt_version>
|
|
||||||
<hoplite_version>1.2.2</hoplite_version>
|
|
||||||
|
|
||||||
<kotlin.code.style>official</kotlin.code.style>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
|
|
||||||
<kotlin.compiler.jvmTarget>12</kotlin.compiler.jvmTarget>
|
|
||||||
<main.class>be.vandewalleh.NotesApplicationKt</main.class>
|
|
||||||
<java.version>14</java.version>
|
|
||||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
|
||||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
|
||||||
</properties>
|
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<id>jcenter</id>
|
|
||||||
<url>https://jcenter.bintray.com</url>
|
|
||||||
<releases>
|
|
||||||
<enabled>true</enabled>
|
|
||||||
</releases>
|
|
||||||
<snapshots>
|
|
||||||
<enabled>true</enabled>
|
|
||||||
</snapshots>
|
|
||||||
</repository>
|
|
||||||
<repository>
|
|
||||||
<id>ktor</id>
|
|
||||||
<url>https://kotlin.bintray.com/ktor</url>
|
|
||||||
<releases>
|
|
||||||
<enabled>true</enabled>
|
|
||||||
</releases>
|
|
||||||
<snapshots>
|
|
||||||
<enabled>true</enabled>
|
|
||||||
</snapshots>
|
|
||||||
</repository>
|
|
||||||
<repository>
|
|
||||||
<id>jitpack</id>
|
|
||||||
<url>https://jitpack.io</url>
|
|
||||||
<releases>
|
|
||||||
<enabled>true</enabled>
|
|
||||||
</releases>
|
|
||||||
<snapshots>
|
|
||||||
<enabled>true</enabled>
|
|
||||||
</snapshots>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
|
||||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
|
||||||
<version>${kotlin_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ktor</groupId>
|
|
||||||
<artifactId>ktor-server-netty</artifactId>
|
|
||||||
<version>${ktor_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>ch.qos.logback</groupId>
|
|
||||||
<artifactId>logback-classic</artifactId>
|
|
||||||
<version>${logback_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ktor</groupId>
|
|
||||||
<artifactId>ktor-server-core</artifactId>
|
|
||||||
<version>${ktor_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ktor</groupId>
|
|
||||||
<artifactId>ktor-jackson</artifactId>
|
|
||||||
<version>${ktor_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ktor</groupId>
|
|
||||||
<artifactId>ktor-auth-jwt</artifactId>
|
|
||||||
<version>${ktor_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ktor</groupId>
|
|
||||||
<artifactId>ktor-client-core</artifactId>
|
|
||||||
<version>${ktor_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.kodein.di</groupId>
|
|
||||||
<artifactId>kodein-di-jvm</artifactId>
|
|
||||||
<version>${kodein_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.junit.jupiter</groupId>
|
|
||||||
<artifactId>junit-jupiter</artifactId>
|
|
||||||
<version>${junit_version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.junit.jupiter</groupId>
|
|
||||||
<artifactId>junit-jupiter-params</artifactId>
|
|
||||||
<version>${junit_version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.mariadb.jdbc</groupId>
|
|
||||||
<artifactId>mariadb-java-client</artifactId>
|
|
||||||
<version>${mariadb_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>me.liuwj.ktorm</groupId>
|
|
||||||
<artifactId>ktorm-core</artifactId>
|
|
||||||
<version>${ktorm_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>me.liuwj.ktorm</groupId>
|
|
||||||
<artifactId>ktorm-support-mysql</artifactId>
|
|
||||||
<version>${ktorm_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>me.liuwj.ktorm</groupId>
|
|
||||||
<artifactId>ktorm-jackson</artifactId>
|
|
||||||
<version>${ktorm_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
|
||||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
|
||||||
<version>2.10.4</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.hekeki</groupId>
|
|
||||||
<artifactId>huckleberry</artifactId>
|
|
||||||
<version>0.0.2-beta</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.flywaydb</groupId>
|
|
||||||
<artifactId>flyway-core</artifactId>
|
|
||||||
<version>${flyway_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.mindrot</groupId>
|
|
||||||
<artifactId>jbcrypt</artifactId>
|
|
||||||
<version>${jbcrypt_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.zaxxer</groupId>
|
|
||||||
<artifactId>HikariCP</artifactId>
|
|
||||||
<version>3.4.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.sksamuel.hoplite</groupId>
|
|
||||||
<artifactId>hoplite-yaml</artifactId>
|
|
||||||
<version>${hoplite_version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>am.ik.yavi</groupId>
|
|
||||||
<artifactId>yavi</artifactId>
|
|
||||||
<version>0.4.0</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.javafaker</groupId>
|
|
||||||
<artifactId>javafaker</artifactId>
|
|
||||||
<version>1.0.2</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ktor</groupId>
|
|
||||||
<artifactId>ktor-server-tests</artifactId>
|
|
||||||
<version>${ktor_version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.testcontainers</groupId>
|
|
||||||
<artifactId>mariadb</artifactId>
|
|
||||||
<version>1.14.3</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.amshove.kluent</groupId>
|
|
||||||
<artifactId>kluent</artifactId>
|
|
||||||
<version>1.61</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.skyscreamer</groupId>
|
|
||||||
<artifactId>jsonassert</artifactId>
|
|
||||||
<version>1.5.0</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.mockk</groupId>
|
|
||||||
<artifactId>mockk</artifactId>
|
|
||||||
<version>1.10.0</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
<build>
|
|
||||||
<sourceDirectory>${project.basedir}/src</sourceDirectory>
|
|
||||||
<testSourceDirectory>${project.basedir}/test</testSourceDirectory>
|
|
||||||
<resources>
|
|
||||||
<resource>
|
|
||||||
<directory>${project.basedir}/resources</directory>
|
|
||||||
</resource>
|
|
||||||
</resources>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.codehaus.mojo</groupId>
|
|
||||||
<artifactId>exec-maven-plugin</artifactId>
|
|
||||||
<version>3.0.0</version>
|
|
||||||
<configuration>
|
|
||||||
<mainClass>${main.class}</mainClass>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
|
||||||
<version>3.0.0-M4</version>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
<version>3.8.1</version>
|
|
||||||
<configuration>
|
|
||||||
<source>14</source>
|
|
||||||
<target>14</target>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<artifactId>kotlin-maven-plugin</artifactId>
|
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
|
||||||
<version>${kotlin_version}</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>compile</id>
|
|
||||||
<goals>
|
|
||||||
<goal>compile</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
<execution>
|
|
||||||
<id>test-compile</id>
|
|
||||||
<goals>
|
|
||||||
<goal>test-compile</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<sourceDirs>
|
|
||||||
<sourceDir>${project.basedir}/test</sourceDir>
|
|
||||||
</sourceDirs>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
|
||||||
<version>3.2.4</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>shade</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<minimizeJar>true</minimizeJar>
|
|
||||||
<transformers>
|
|
||||||
<transformer
|
|
||||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
|
||||||
<mainClass>${main.class}</mainClass>
|
|
||||||
</transformer>
|
|
||||||
</transformers>
|
|
||||||
<artifactSet>
|
|
||||||
<excludes>
|
|
||||||
<exclude>io.ktor:ktor-client-*</exclude>
|
|
||||||
</excludes>
|
|
||||||
</artifactSet>
|
|
||||||
<filters>
|
|
||||||
<filter>
|
|
||||||
<artifact>org.jetbrains.kotlin:kotlin-reflect</artifact>
|
|
||||||
<includes>
|
|
||||||
<include>**</include>
|
|
||||||
</includes>
|
|
||||||
</filter>
|
|
||||||
<filter>
|
|
||||||
<artifact>org.mariadb.jdbc:mariadb-java-client</artifact>
|
|
||||||
<includes>
|
|
||||||
<include>**</include>
|
|
||||||
</includes>
|
|
||||||
</filter>
|
|
||||||
<filter>
|
|
||||||
<artifact>*:*</artifact>
|
|
||||||
<excludes>
|
|
||||||
<exclude>META-INF/maven/**</exclude>
|
|
||||||
<exclude>META-INF/proguard/**</exclude>
|
|
||||||
<exclude>META-INF/native-image/**</exclude>
|
|
||||||
<exclude>META-INF/*.kotlin_module</exclude>
|
|
||||||
</excludes>
|
|
||||||
</filter>
|
|
||||||
</filters>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
database:
|
|
||||||
host: ${MYSQL_HOST:-localhost}
|
|
||||||
port: ${MYSQL_PORT:-3306}
|
|
||||||
name: ${MYSQL_DATABASE:-notes}
|
|
||||||
username: ${MYSQL_USER:-notes}
|
|
||||||
password: ${MYSQL_PASSWORD:-notes}
|
|
||||||
|
|
||||||
server:
|
|
||||||
host: ${HOST:-127.0.0.1}
|
|
||||||
port: ${PORT:-8081}
|
|
||||||
cors: ${CORS:-true}
|
|
||||||
|
|
||||||
jwt:
|
|
||||||
auth:
|
|
||||||
secret: ${JWT_SECRET:-uiqzRNiMYwbObn/Ps5xTasYVeu/63ZuI+1oB98Ez+lY=} # Can be generated with `openssl rand -base64 32`
|
|
||||||
validity: 24
|
|
||||||
unit: HOURS
|
|
||||||
refresh:
|
|
||||||
secret: ${JWT_REFRESH_SECRET=-wWchkx44YGig4Q5Z7b7+E/3ymGEGd6PS7UGedMul3bg=} # Can be generated with `openssl rand -base64 32`
|
|
||||||
validity: 15
|
|
||||||
unit: DAYS
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package be.vandewalleh
|
|
||||||
|
|
||||||
import com.sksamuel.hoplite.Masked
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
data class Config(val database: DatabaseConfig, val server: ServerConfig, val jwt: JwtConfig) {
|
|
||||||
override fun toString(): String {
|
|
||||||
return """
|
|
||||||
Config(
|
|
||||||
database=$database,
|
|
||||||
server=$server,
|
|
||||||
jwt=$jwt
|
|
||||||
)
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DatabaseConfig(val host: String, val port: Int, val name: String, val username: String, val password: Masked)
|
|
||||||
data class ServerConfig(val host: String, val port: Int, val cors: Boolean)
|
|
||||||
data class JwtConfig(val auth: Jwt, val refresh: Jwt)
|
|
||||||
data class Jwt(val validity: Long, val unit: TimeUnit, val secret: Masked)
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package be.vandewalleh
|
|
||||||
|
|
||||||
import be.vandewalleh.auth.AuthenticationModule
|
|
||||||
import be.vandewalleh.auth.SimpleJWT
|
|
||||||
import be.vandewalleh.extensions.ApplicationBuilder
|
|
||||||
import be.vandewalleh.extensions.RoutingBuilder
|
|
||||||
import be.vandewalleh.factories.configurationFactory
|
|
||||||
import be.vandewalleh.factories.dataSourceFactory
|
|
||||||
import be.vandewalleh.factories.databaseFactory
|
|
||||||
import be.vandewalleh.factories.simpleJwtFactory
|
|
||||||
import be.vandewalleh.features.*
|
|
||||||
import be.vandewalleh.routing.NoteRoutes
|
|
||||||
import be.vandewalleh.routing.TagRoutes
|
|
||||||
import be.vandewalleh.routing.UserRoutes
|
|
||||||
import be.vandewalleh.services.NoteService
|
|
||||||
import be.vandewalleh.services.UserService
|
|
||||||
import org.kodein.di.*
|
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
val mainModule = DI.Module("main") {
|
|
||||||
bind() from singleton { NoteService(instance()) }
|
|
||||||
bind() from singleton { UserService(instance(), instance()) }
|
|
||||||
|
|
||||||
bind() from singleton { configurationFactory() }
|
|
||||||
|
|
||||||
bind() from setBinding<ApplicationBuilder>()
|
|
||||||
bind<ApplicationBuilder>().inSet() with singleton { ErrorHandler() }
|
|
||||||
bind<ApplicationBuilder>().inSet() with singleton { ContentNegotiationFeature() }
|
|
||||||
bind<ApplicationBuilder>().inSet() with singleton { CorsFeature(instance<Config>().server.cors) }
|
|
||||||
bind<ApplicationBuilder>().inSet() with singleton { AuthenticationModule(instance(tag = "auth")) }
|
|
||||||
bind<ApplicationBuilder>().inSet() with singleton { MigrationHook(instance()) }
|
|
||||||
bind<ApplicationBuilder>().inSet() with singleton { ShutdownDatabaseConnection(instance()) }
|
|
||||||
|
|
||||||
bind() from setBinding<RoutingBuilder>()
|
|
||||||
bind<RoutingBuilder>().inSet() with singleton { NoteRoutes(instance()) }
|
|
||||||
bind<RoutingBuilder>().inSet() with singleton { TagRoutes(instance()) }
|
|
||||||
bind<RoutingBuilder>().inSet() with singleton {
|
|
||||||
UserRoutes(
|
|
||||||
instance(tag = "auth"),
|
|
||||||
instance(tag = "refresh"),
|
|
||||||
instance(),
|
|
||||||
instance()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind<SimpleJWT>(tag = "auth") with singleton { simpleJwtFactory(instance<Config>().jwt.auth) }
|
|
||||||
bind<SimpleJWT>(tag = "refresh") with singleton { simpleJwtFactory(instance<Config>().jwt.refresh) }
|
|
||||||
|
|
||||||
bind() from singleton { LoggerFactory.getLogger("Application") }
|
|
||||||
bind() from singleton { dataSourceFactory(instance<Config>().database) }
|
|
||||||
bind() from singleton { databaseFactory(instance()) }
|
|
||||||
bind() from singleton { Migration(instance()) }
|
|
||||||
|
|
||||||
bind<PasswordHash>() with singleton { BcryptPasswordHash() }
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package be.vandewalleh
|
|
||||||
|
|
||||||
import be.vandewalleh.extensions.ApplicationBuilder
|
|
||||||
import be.vandewalleh.extensions.RoutingBuilder
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.routing.*
|
|
||||||
import io.ktor.server.engine.*
|
|
||||||
import io.ktor.server.netty.*
|
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.kodein.di.description
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
val di = DI { import(mainModule) }
|
|
||||||
val config by di.instance<Config>()
|
|
||||||
val logger by di.instance<Logger>()
|
|
||||||
logger.info("Running application with configuration $config")
|
|
||||||
logger.debug("Kodein bindings\n${di.container.tree.bindings.description()}")
|
|
||||||
serve(di)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun serve(di: DI) {
|
|
||||||
val config by di.instance<Config>()
|
|
||||||
val logger by di.instance<Logger>()
|
|
||||||
val env = applicationEngineEnvironment {
|
|
||||||
module {
|
|
||||||
module(di)
|
|
||||||
}
|
|
||||||
log = logger
|
|
||||||
connector {
|
|
||||||
host = config.server.host
|
|
||||||
port = config.server.port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
with(embeddedServer(Netty, env)) {
|
|
||||||
addShutdownHook { stop(1, 5, TimeUnit.SECONDS) }
|
|
||||||
start(wait = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Application.module(di: DI) {
|
|
||||||
val builders: Set<ApplicationBuilder> by di.instance()
|
|
||||||
|
|
||||||
builders.forEach {
|
|
||||||
it.builder(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
val routingBuilders: Set<RoutingBuilder> by di.instance()
|
|
||||||
routingBuilders.forEach {
|
|
||||||
routing(it.builder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package be.vandewalleh.auth
|
|
||||||
|
|
||||||
import be.vandewalleh.extensions.ApplicationBuilder
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.auth.*
|
|
||||||
import io.ktor.auth.jwt.*
|
|
||||||
|
|
||||||
class AuthenticationModule(authJwt: SimpleJWT) : ApplicationBuilder({
|
|
||||||
install(Authentication) {
|
|
||||||
jwt {
|
|
||||||
verifier(authJwt.verifier)
|
|
||||||
validate {
|
|
||||||
UserIdPrincipal(it.payload.getClaim("id").asInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package be.vandewalleh.auth
|
|
||||||
|
|
||||||
import com.auth0.jwt.JWT
|
|
||||||
import com.auth0.jwt.JWTVerifier
|
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class SimpleJWT(secret: String, validity: Long, unit: TimeUnit) {
|
|
||||||
private val validityInMs = TimeUnit.MILLISECONDS.convert(validity, unit)
|
|
||||||
private val algorithm = Algorithm.HMAC256(secret)
|
|
||||||
|
|
||||||
val verifier: JWTVerifier = JWT.require(algorithm).build()
|
|
||||||
fun sign(id: Int): String = JWT.create()
|
|
||||||
.withClaim("id", id)
|
|
||||||
.withExpiresAt(getExpiration())
|
|
||||||
.sign(algorithm)
|
|
||||||
|
|
||||||
private fun getExpiration() = Date(System.currentTimeMillis() + validityInMs)
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package be.vandewalleh.auth
|
|
||||||
|
|
||||||
import io.ktor.auth.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a simple user's principal identified by [id]
|
|
||||||
* @property id of the user
|
|
||||||
*/
|
|
||||||
data class UserIdPrincipal(val id: Int) : Principal
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package be.vandewalleh.auth
|
|
||||||
|
|
||||||
import io.ktor.auth.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a simple user [username] and [password] credential pair
|
|
||||||
* @property username
|
|
||||||
* @property password
|
|
||||||
*/
|
|
||||||
data class UsernamePasswordCredential(val username: String, val password: String) : Credential
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package be.vandewalleh.entities
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
|
||||||
import me.liuwj.ktorm.entity.*
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
interface Note : Entity<Note> {
|
|
||||||
companion object : Entity.Factory<Note>()
|
|
||||||
|
|
||||||
var uuid: UUID
|
|
||||||
var title: String
|
|
||||||
var content: String
|
|
||||||
var updatedAt: LocalDateTime
|
|
||||||
|
|
||||||
@get:JsonIgnore
|
|
||||||
var user: User
|
|
||||||
|
|
||||||
// Not part of the Notes table
|
|
||||||
var tags: List<String>
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package be.vandewalleh.entities
|
|
||||||
|
|
||||||
import me.liuwj.ktorm.entity.*
|
|
||||||
|
|
||||||
interface Tag : Entity<Tag> {
|
|
||||||
companion object : Entity.Factory<Tag>()
|
|
||||||
|
|
||||||
val id: Int
|
|
||||||
var name: String
|
|
||||||
var note: Note
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package be.vandewalleh.entities
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
|
||||||
import me.liuwj.ktorm.entity.*
|
|
||||||
|
|
||||||
interface User : Entity<User> {
|
|
||||||
companion object : Entity.Factory<User>()
|
|
||||||
|
|
||||||
@get:JsonIgnore
|
|
||||||
val id: Int
|
|
||||||
|
|
||||||
var username: String
|
|
||||||
|
|
||||||
@get:JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
|
||||||
var password: String
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package be.vandewalleh.extensions
|
|
||||||
|
|
||||||
import be.vandewalleh.auth.UserIdPrincipal
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.auth.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.response.*
|
|
||||||
|
|
||||||
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
|
|
||||||
respondText(
|
|
||||||
"""{"status": "${status.description}"}""",
|
|
||||||
status = status,
|
|
||||||
contentType = ContentType.Application.Json
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the userId for the currently authenticated user
|
|
||||||
*/
|
|
||||||
fun ApplicationCall.authenticatedUserId() = principal<UserIdPrincipal>()!!.id
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package be.vandewalleh.extensions
|
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
suspend inline fun <T> launchIo(crossinline block: () -> T): T =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package be.vandewalleh.extensions
|
|
||||||
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.routing.*
|
|
||||||
|
|
||||||
abstract class RoutingBuilder(val builder: Routing.() -> Unit)
|
|
||||||
|
|
||||||
abstract class ApplicationBuilder(val builder: Application.() -> Unit)
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package be.vandewalleh.extensions
|
|
||||||
|
|
||||||
import be.vandewalleh.tables.Notes
|
|
||||||
import be.vandewalleh.tables.Tags
|
|
||||||
import be.vandewalleh.tables.Users
|
|
||||||
import me.liuwj.ktorm.database.*
|
|
||||||
import me.liuwj.ktorm.entity.*
|
|
||||||
import me.liuwj.ktorm.schema.*
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.sql.PreparedStatement
|
|
||||||
import java.sql.ResultSet
|
|
||||||
import java.sql.Types
|
|
||||||
import java.util.UUID as JavaUUID
|
|
||||||
|
|
||||||
class UuidBinarySqlType : SqlType<JavaUUID>(Types.BINARY, typeName = "uuidBinary") {
|
|
||||||
override fun doGetResult(rs: ResultSet, index: Int): JavaUUID? {
|
|
||||||
val value = rs.getBytes(index) ?: return null
|
|
||||||
return ByteBuffer.wrap(value).let { b -> JavaUUID(b.long, b.long) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: JavaUUID) {
|
|
||||||
val bytes = ByteBuffer.allocate(16)
|
|
||||||
.putLong(parameter.mostSignificantBits)
|
|
||||||
.putLong(parameter.leastSignificantBits)
|
|
||||||
.array()
|
|
||||||
ps.setBytes(index, bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <E : Any> BaseTable<E>.uuidBinary(name: String): Column<JavaUUID> {
|
|
||||||
return registerColumn(name, UuidBinarySqlType())
|
|
||||||
}
|
|
||||||
|
|
||||||
val Database.users get() = this.sequenceOf(Users, withReferences = false)
|
|
||||||
val Database.notes get() = this.sequenceOf(Notes, withReferences = false)
|
|
||||||
val Database.tags get() = this.sequenceOf(Tags, withReferences = false)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package be.vandewalleh.factories
|
|
||||||
|
|
||||||
import be.vandewalleh.Config
|
|
||||||
import com.sksamuel.hoplite.ConfigLoader
|
|
||||||
|
|
||||||
fun configurationFactory() =
|
|
||||||
ConfigLoader().loadConfigOrThrow<Config>("/application.yaml")
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package be.vandewalleh.factories
|
|
||||||
|
|
||||||
import be.vandewalleh.DatabaseConfig
|
|
||||||
import com.zaxxer.hikari.HikariConfig
|
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
|
||||||
|
|
||||||
fun dataSourceFactory(config: DatabaseConfig): HikariDataSource {
|
|
||||||
val host = config.host
|
|
||||||
val port = config.port
|
|
||||||
val name = config.name
|
|
||||||
|
|
||||||
val hikariConfig = HikariConfig().apply {
|
|
||||||
jdbcUrl = "jdbc:mariadb://$host:$port/$name"
|
|
||||||
username = config.username
|
|
||||||
password = config.password.value
|
|
||||||
connectionTimeout = 3000 // 3 seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
return HikariDataSource(hikariConfig)
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package be.vandewalleh.factories
|
|
||||||
|
|
||||||
import me.liuwj.ktorm.database.*
|
|
||||||
import javax.sql.DataSource
|
|
||||||
|
|
||||||
fun databaseFactory(dataSource: DataSource) = Database.connect(dataSource)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package be.vandewalleh.factories
|
|
||||||
|
|
||||||
import be.vandewalleh.Jwt
|
|
||||||
import be.vandewalleh.auth.SimpleJWT
|
|
||||||
|
|
||||||
fun simpleJwtFactory(jwt: Jwt) = SimpleJWT(jwt.secret.value, jwt.validity, jwt.unit)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package be.vandewalleh.features
|
|
||||||
|
|
||||||
import be.vandewalleh.extensions.ApplicationBuilder
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
|
||||||
import com.fasterxml.jackson.databind.util.StdDateFormat
|
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.features.*
|
|
||||||
import io.ktor.jackson.*
|
|
||||||
import me.liuwj.ktorm.jackson.*
|
|
||||||
|
|
||||||
class ContentNegotiationFeature : ApplicationBuilder({
|
|
||||||
install(ContentNegotiation) {
|
|
||||||
jackson {
|
|
||||||
registerModule(KtormModule())
|
|
||||||
registerModule(JavaTimeModule())
|
|
||||||
disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT)
|
|
||||||
dateFormat = StdDateFormat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package be.vandewalleh.features
|
|
||||||
|
|
||||||
import be.vandewalleh.extensions.ApplicationBuilder
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.features.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
|
|
||||||
class CorsFeature(enabled: Boolean) : ApplicationBuilder({
|
|
||||||
if (enabled) {
|
|
||||||
install(CORS) {
|
|
||||||
anyHost()
|
|
||||||
header(HttpHeaders.ContentType)
|
|
||||||
header(HttpHeaders.Authorization)
|
|
||||||
methods.add(HttpMethod.Delete)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package be.vandewalleh.features
|
|
||||||
|
|
||||||
import be.vandewalleh.extensions.ApplicationBuilder
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.features.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.response.*
|
|
||||||
import io.ktor.utils.io.errors.*
|
|
||||||
import java.sql.SQLTransientConnectionException
|
|
||||||
|
|
||||||
class ErrorHandler : ApplicationBuilder({
|
|
||||||
install(StatusPages) {
|
|
||||||
|
|
||||||
jacksonErrors()
|
|
||||||
|
|
||||||
exception<IOException> {
|
|
||||||
call.respond(HttpStatusCode.BadRequest)
|
|
||||||
}
|
|
||||||
exception<ValidationException> {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ErrorResponse(it.error))
|
|
||||||
}
|
|
||||||
|
|
||||||
exception<SQLTransientConnectionException> {
|
|
||||||
val error = mapOf("error" to "It seems the server can't connect to the database")
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
class ValidationException(val error: String) : RuntimeException()
|
|
||||||
class ErrorResponse(val error: String)
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package be.vandewalleh.features
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonParseException
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException
|
|
||||||
import com.fasterxml.jackson.core.exc.InputCoercionException
|
|
||||||
import com.fasterxml.jackson.databind.JsonMappingException
|
|
||||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException
|
|
||||||
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
|
|
||||||
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.features.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.response.*
|
|
||||||
|
|
||||||
fun StatusPages.Configuration.jacksonErrors() {
|
|
||||||
exception<MismatchedInputException> {
|
|
||||||
val error = InvalidFormatError(it.path.firstOrNull()?.fieldName, it.targetType)
|
|
||||||
call.respond(HttpStatusCode.BadRequest, error)
|
|
||||||
}
|
|
||||||
exception<JsonParseException> {
|
|
||||||
val error = JsonParseError()
|
|
||||||
call.respond(HttpStatusCode.BadRequest, error)
|
|
||||||
}
|
|
||||||
exception<UnrecognizedPropertyException> {
|
|
||||||
val error = UnrecognizedPropertyError(it.path[0].fieldName)
|
|
||||||
call.respond(HttpStatusCode.BadRequest, error)
|
|
||||||
}
|
|
||||||
exception<MissingKotlinParameterException> {
|
|
||||||
val error = MissingKotlinParameterError(it.path[0].fieldName)
|
|
||||||
call.respond(HttpStatusCode.BadRequest, error)
|
|
||||||
}
|
|
||||||
exception<JsonProcessingException> {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, JsonProcessingError())
|
|
||||||
}
|
|
||||||
exception<JsonMappingException> {
|
|
||||||
if (it.cause is InputCoercionException) {
|
|
||||||
return@exception call.respond(HttpStatusCode.BadRequest, OutOfRangeError(it.path[0].fieldName))
|
|
||||||
}
|
|
||||||
call.respond(HttpStatusCode.BadRequest, JsonProcessingError())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InvalidFormatError(val value: Any?, targetType: Class<*>) {
|
|
||||||
val msg = "Wrong type"
|
|
||||||
val required = targetType.simpleName
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnrecognizedPropertyError(val field: Any?) {
|
|
||||||
val msg = "Unrecognized field"
|
|
||||||
}
|
|
||||||
|
|
||||||
class MissingKotlinParameterError(val field: String) {
|
|
||||||
val msg = "Missing field"
|
|
||||||
}
|
|
||||||
|
|
||||||
class JsonProcessingError {
|
|
||||||
val msg = "An error occurred while processing JSON"
|
|
||||||
}
|
|
||||||
|
|
||||||
class JsonParseError {
|
|
||||||
val msg = "Invalid JSON"
|
|
||||||
}
|
|
||||||
|
|
||||||
class OutOfRangeError(val field: String) {
|
|
||||||
val msg = "Numeric value out of range"
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package be.vandewalleh.features
|
|
||||||
|
|
||||||
import be.vandewalleh.extensions.ApplicationBuilder
|
|
||||||
import io.ktor.application.*
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.flywaydb.core.Flyway
|
|
||||||
import javax.sql.DataSource
|
|
||||||
|
|
||||||
class MigrationHook(migration: Migration) : ApplicationBuilder({
|
|
||||||
environment.monitor.subscribe(ApplicationStarted) {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
migration.migrate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
class Migration(private val dataSource: DataSource) {
|
|
||||||
fun migrate() {
|
|
||||||
Flyway.configure()
|
|
||||||
.dataSource(dataSource)
|
|
||||||
.baselineOnMigrate(true)
|
|
||||||
.load()
|
|
||||||
.migrate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package be.vandewalleh.features
|
|
||||||
|
|
||||||
import org.mindrot.jbcrypt.BCrypt
|
|
||||||
|
|
||||||
interface PasswordHash {
|
|
||||||
fun crypt(password: String): String
|
|
||||||
fun verify(password: String, hashedPassword: String): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
class BcryptPasswordHash : PasswordHash {
|
|
||||||
override fun crypt(password: String) = BCrypt.hashpw(password, BCrypt.gensalt())!!
|
|
||||||
override fun verify(password: String, hashedPassword: String) = BCrypt.checkpw(password, hashedPassword)
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package be.vandewalleh.features
|
|
||||||
|
|
||||||
import be.vandewalleh.extensions.ApplicationBuilder
|
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
|
||||||
import io.ktor.application.*
|
|
||||||
|
|
||||||
class ShutdownDatabaseConnection(hikariDataSource: HikariDataSource) : ApplicationBuilder({
|
|
||||||
environment.monitor.subscribe(ApplicationStopPreparing) {
|
|
||||||
if (!hikariDataSource.isClosed) {
|
|
||||||
hikariDataSource.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package be.vandewalleh.routing
|
|
||||||
|
|
||||||
import be.vandewalleh.extensions.RoutingBuilder
|
|
||||||
import be.vandewalleh.extensions.authenticatedUserId
|
|
||||||
import be.vandewalleh.extensions.respondStatus
|
|
||||||
import be.vandewalleh.features.ValidationException
|
|
||||||
import be.vandewalleh.services.NoteService
|
|
||||||
import be.vandewalleh.validation.noteValidator
|
|
||||||
import be.vandewalleh.validation.receiveValidated
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.auth.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.response.*
|
|
||||||
import io.ktor.routing.*
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class NoteRoutes(noteService: NoteService) : RoutingBuilder({
|
|
||||||
authenticate {
|
|
||||||
route("/notes") {
|
|
||||||
createNote(noteService)
|
|
||||||
getAllNotes(noteService)
|
|
||||||
|
|
||||||
route("/{uuid}") {
|
|
||||||
getNote(noteService)
|
|
||||||
updateNote(noteService)
|
|
||||||
deleteNote(noteService)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
private fun Route.createNote(noteService: NoteService) {
|
|
||||||
post {
|
|
||||||
val userId = call.authenticatedUserId()
|
|
||||||
val note = call.receiveValidated(noteValidator)
|
|
||||||
val createdNote = noteService.create(userId, note)
|
|
||||||
call.respond(HttpStatusCode.Created, createdNote)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Route.getAllNotes(noteService: NoteService) {
|
|
||||||
get {
|
|
||||||
val userId = call.authenticatedUserId()
|
|
||||||
val limit = call.parameters["limit"]?.toInt() ?: 20 // FIXME validate
|
|
||||||
val after = call.parameters["after"]?.let { UUID.fromString(it) } // FIXME validate
|
|
||||||
val notes = noteService.findAll(userId, limit, after)
|
|
||||||
call.respond(notes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Route.getNote(noteService: NoteService) {
|
|
||||||
get {
|
|
||||||
val userId = call.authenticatedUserId()
|
|
||||||
val noteUuid = call.noteUuid()
|
|
||||||
|
|
||||||
val response = noteService.find(userId, noteUuid)
|
|
||||||
?: return@get call.respondStatus(HttpStatusCode.NotFound)
|
|
||||||
call.respond(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Route.updateNote(noteService: NoteService) {
|
|
||||||
put {
|
|
||||||
val userId = call.authenticatedUserId()
|
|
||||||
val noteUuid = call.noteUuid()
|
|
||||||
|
|
||||||
val note = call.receiveValidated(noteValidator)
|
|
||||||
note.uuid = noteUuid
|
|
||||||
|
|
||||||
if (noteService.updateNote(userId, note))
|
|
||||||
call.respondStatus(HttpStatusCode.OK)
|
|
||||||
else call.respondStatus(HttpStatusCode.NotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Route.deleteNote(noteService: NoteService) {
|
|
||||||
delete {
|
|
||||||
val userId = call.authenticatedUserId()
|
|
||||||
val noteUuid = call.noteUuid()
|
|
||||||
|
|
||||||
if (noteService.delete(userId, noteUuid))
|
|
||||||
call.respondStatus(HttpStatusCode.OK)
|
|
||||||
else
|
|
||||||
call.respondStatus(HttpStatusCode.NotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ApplicationCall.noteUuid(): UUID {
|
|
||||||
val uuid = parameters["uuid"]
|
|
||||||
return try {
|
|
||||||
UUID.fromString(uuid)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
throw ValidationException("`$uuid` is not a valid UUID")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package be.vandewalleh.routing
|
|
||||||
|
|
||||||
import be.vandewalleh.extensions.RoutingBuilder
|
|
||||||
import be.vandewalleh.extensions.authenticatedUserId
|
|
||||||
import be.vandewalleh.services.NoteService
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.auth.*
|
|
||||||
import io.ktor.response.*
|
|
||||||
import io.ktor.routing.*
|
|
||||||
|
|
||||||
class TagRoutes(noteService: NoteService) : RoutingBuilder({
|
|
||||||
authenticate {
|
|
||||||
get("/tags") {
|
|
||||||
call.respond(noteService.getTags(call.authenticatedUserId()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
package be.vandewalleh.routing
|
|
||||||
|
|
||||||
import be.vandewalleh.auth.SimpleJWT
|
|
||||||
import be.vandewalleh.auth.UsernamePasswordCredential
|
|
||||||
import be.vandewalleh.extensions.RoutingBuilder
|
|
||||||
import be.vandewalleh.extensions.authenticatedUserId
|
|
||||||
import be.vandewalleh.extensions.respondStatus
|
|
||||||
import be.vandewalleh.features.PasswordHash
|
|
||||||
import be.vandewalleh.services.UserService
|
|
||||||
import be.vandewalleh.validation.receiveValidated
|
|
||||||
import be.vandewalleh.validation.registerValidator
|
|
||||||
import com.auth0.jwt.exceptions.JWTVerificationException
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.auth.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.request.*
|
|
||||||
import io.ktor.response.*
|
|
||||||
import io.ktor.routing.*
|
|
||||||
|
|
||||||
class UserRoutes(
|
|
||||||
authJWT: SimpleJWT,
|
|
||||||
refreshJWT: SimpleJWT,
|
|
||||||
userService: UserService,
|
|
||||||
passwordHash: PasswordHash
|
|
||||||
) : RoutingBuilder({
|
|
||||||
route("/user") {
|
|
||||||
createUser(userService)
|
|
||||||
route("/login") {
|
|
||||||
login(userService, passwordHash, authJWT, refreshJWT)
|
|
||||||
}
|
|
||||||
route("/refresh_token") {
|
|
||||||
refreshToken(userService, authJWT, refreshJWT)
|
|
||||||
}
|
|
||||||
authenticate {
|
|
||||||
deleteUser(userService)
|
|
||||||
route("/me") {
|
|
||||||
userInfo(userService)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
private fun Route.userInfo(userService: UserService) {
|
|
||||||
get {
|
|
||||||
val id = call.authenticatedUserId()
|
|
||||||
val info = userService.find(id)
|
|
||||||
if (info != null) call.respond(mapOf("user" to info))
|
|
||||||
else call.respondStatus(HttpStatusCode.Unauthorized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Route.deleteUser(userService: UserService) {
|
|
||||||
delete {
|
|
||||||
val userId = call.authenticatedUserId()
|
|
||||||
call.respondStatus(
|
|
||||||
if (userService.delete(userId)) HttpStatusCode.OK
|
|
||||||
else HttpStatusCode.NotFound
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Route.createUser(userService: UserService) {
|
|
||||||
post {
|
|
||||||
val user = call.receiveValidated(registerValidator)
|
|
||||||
|
|
||||||
if (userService.exists(user.username))
|
|
||||||
return@post call.respondStatus(HttpStatusCode.Conflict)
|
|
||||||
|
|
||||||
val newUser = userService.create(user.username, user.password)
|
|
||||||
?: return@post call.respondStatus(HttpStatusCode.Conflict)
|
|
||||||
|
|
||||||
call.respond(HttpStatusCode.Created, newUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Route.login(
|
|
||||||
userService: UserService,
|
|
||||||
passwordHash: PasswordHash,
|
|
||||||
authJWT: SimpleJWT,
|
|
||||||
refreshJWT: SimpleJWT
|
|
||||||
) {
|
|
||||||
post {
|
|
||||||
val credential = call.receive<UsernamePasswordCredential>()
|
|
||||||
|
|
||||||
val user = userService.find(credential.username)
|
|
||||||
?: return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
|
||||||
|
|
||||||
if (!passwordHash.verify(credential.password, user.password)) {
|
|
||||||
return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = DualToken(
|
|
||||||
token = authJWT.sign(user.id),
|
|
||||||
refreshToken = refreshJWT.sign(user.id)
|
|
||||||
)
|
|
||||||
return@post call.respond(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Route.refreshToken(userService: UserService, authJWT: SimpleJWT, refreshJWT: SimpleJWT) {
|
|
||||||
post {
|
|
||||||
val token = call.receive<RefreshToken>().refreshToken
|
|
||||||
|
|
||||||
val id = try {
|
|
||||||
val decodedJWT = refreshJWT.verifier.verify(token)
|
|
||||||
decodedJWT.getClaim("id").asInt()
|
|
||||||
} catch (e: JWTVerificationException) {
|
|
||||||
return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userService.exists(id))
|
|
||||||
return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
|
||||||
|
|
||||||
val response = DualToken(
|
|
||||||
token = authJWT.sign(id),
|
|
||||||
refreshToken = refreshJWT.sign(id)
|
|
||||||
)
|
|
||||||
return@post call.respond(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class RefreshToken(val refreshToken: String)
|
|
||||||
private data class DualToken(val token: String, val refreshToken: String)
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
package be.vandewalleh.services
|
|
||||||
|
|
||||||
import be.vandewalleh.entities.Note
|
|
||||||
import be.vandewalleh.extensions.launchIo
|
|
||||||
import be.vandewalleh.extensions.notes
|
|
||||||
import be.vandewalleh.extensions.tags
|
|
||||||
import be.vandewalleh.tables.Notes
|
|
||||||
import be.vandewalleh.tables.Tags
|
|
||||||
import me.liuwj.ktorm.database.*
|
|
||||||
import me.liuwj.ktorm.dsl.*
|
|
||||||
import me.liuwj.ktorm.entity.*
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.HashMap
|
|
||||||
|
|
||||||
/**
|
|
||||||
* service to handle database queries at the Notes level.
|
|
||||||
*/
|
|
||||||
class NoteService(private val db: Database) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns a list of [Note] associated with the userId
|
|
||||||
*/
|
|
||||||
suspend fun findAll(userId: Int, limit: Int = 20, after: UUID? = null): List<Note> = launchIo {
|
|
||||||
|
|
||||||
var previous: LocalDateTime? = null
|
|
||||||
|
|
||||||
if (after != null) {
|
|
||||||
previous = db.notes
|
|
||||||
.filter { it.userId eq userId and (it.uuid eq after) }
|
|
||||||
.mapColumns { it.updatedAt }
|
|
||||||
.firstOrNull() ?: return@launchIo emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val notes = db.notes
|
|
||||||
.filterColumns { it.columns - it.userId }
|
|
||||||
.filter {
|
|
||||||
if (previous == null) it.userId eq userId
|
|
||||||
else (it.userId eq userId) and (it.updatedAt less previous)
|
|
||||||
}
|
|
||||||
.sortedByDescending { it.updatedAt }
|
|
||||||
.take(limit)
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
if (notes.isEmpty()) return@launchIo emptyList()
|
|
||||||
|
|
||||||
val tagsByUuid =
|
|
||||||
db.tags
|
|
||||||
.filterColumns { listOf(it.noteUuid, it.name) }
|
|
||||||
.filter { it.noteUuid inList notes.map { note -> note.uuid } }
|
|
||||||
.groupByTo(HashMap(), { it.note.uuid }, { it.name })
|
|
||||||
|
|
||||||
notes.forEach {
|
|
||||||
val tags = tagsByUuid[it.uuid]
|
|
||||||
if (tags != null) it.tags = tags
|
|
||||||
}
|
|
||||||
|
|
||||||
notes
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun exists(userId: Int, uuid: UUID) = launchIo {
|
|
||||||
db.notes.any { (it.userId eq userId) and (it.uuid eq uuid) }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun create(userId: Int, note: Note): Note = launchIo {
|
|
||||||
val uuid = UUID.randomUUID()
|
|
||||||
val newNote = note.copy().apply {
|
|
||||||
this["uuid"] = uuid
|
|
||||||
this.user["id"] = userId
|
|
||||||
this.updatedAt = LocalDateTime.now()
|
|
||||||
}
|
|
||||||
db.useTransaction {
|
|
||||||
db.notes.add(newNote)
|
|
||||||
db.batchInsert(Tags) {
|
|
||||||
note.tags.forEach { tagName ->
|
|
||||||
item {
|
|
||||||
it.noteUuid to uuid
|
|
||||||
it.name to tagName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newNote
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
suspend fun find(userId: Int, noteUuid: UUID): Note? = launchIo {
|
|
||||||
val note =
|
|
||||||
db.notes
|
|
||||||
.filterColumns { it.columns - it.userId }
|
|
||||||
.filter { it.uuid eq noteUuid }
|
|
||||||
.find { it.userId eq userId }
|
|
||||||
?: return@launchIo null
|
|
||||||
|
|
||||||
val tags =
|
|
||||||
db.sequenceOf(Tags, withReferences = false)
|
|
||||||
.filter { it.noteUuid eq noteUuid }
|
|
||||||
.mapColumns { it.name } as List<String>
|
|
||||||
|
|
||||||
note.also { it.tags = tags }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateNote(userId: Int, note: Note): Boolean = launchIo {
|
|
||||||
if (note["uuid"] == null) error("UUID is required")
|
|
||||||
|
|
||||||
db.useTransaction {
|
|
||||||
val currentNote = db.notes
|
|
||||||
.find { it.uuid eq note.uuid and (it.userId eq userId) }
|
|
||||||
?: return@launchIo false
|
|
||||||
|
|
||||||
currentNote.title = note.title
|
|
||||||
currentNote.content = note.content
|
|
||||||
currentNote.updatedAt = LocalDateTime.now()
|
|
||||||
currentNote.flushChanges()
|
|
||||||
|
|
||||||
// delete all tags
|
|
||||||
db.delete(Tags) {
|
|
||||||
it.noteUuid eq note.uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
// put new ones
|
|
||||||
note.tags.forEach { tagName ->
|
|
||||||
db.insert(Tags) {
|
|
||||||
it.name to tagName
|
|
||||||
it.noteUuid to note.uuid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun delete(userId: Int, noteUuid: UUID): Boolean = launchIo {
|
|
||||||
db.useTransaction {
|
|
||||||
db.delete(Notes) { it.uuid eq noteUuid and (it.userId eq userId) } == 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
suspend fun getTags(userId: Int): List<String> = launchIo {
|
|
||||||
db.sequenceOf(Tags)
|
|
||||||
.filter { it.note.userId eq userId }
|
|
||||||
.mapColumns(isDistinct = true) { it.name } as List<String>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package be.vandewalleh.services
|
|
||||||
|
|
||||||
import be.vandewalleh.entities.User
|
|
||||||
import be.vandewalleh.extensions.launchIo
|
|
||||||
import be.vandewalleh.extensions.users
|
|
||||||
import be.vandewalleh.features.PasswordHash
|
|
||||||
import be.vandewalleh.tables.Users
|
|
||||||
import me.liuwj.ktorm.database.*
|
|
||||||
import me.liuwj.ktorm.dsl.*
|
|
||||||
import me.liuwj.ktorm.entity.*
|
|
||||||
import java.sql.SQLIntegrityConstraintViolationException
|
|
||||||
|
|
||||||
/**
|
|
||||||
* service to handle database queries for users.
|
|
||||||
*/
|
|
||||||
class UserService(private val db: Database, private val passwordHash: PasswordHash) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns a user from it's username if found or null
|
|
||||||
*/
|
|
||||||
suspend fun find(username: String): User? = launchIo {
|
|
||||||
db.users.find { it.username eq username }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun find(id: Int): User? = launchIo {
|
|
||||||
db.users.find { it.id eq id }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun exists(username: String) = launchIo {
|
|
||||||
db.users.any { it.username eq username }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun exists(userId: Int) = launchIo {
|
|
||||||
db.users.any { it.id eq userId }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun create(username: String, password: String): User? {
|
|
||||||
val newUser = User {
|
|
||||||
this.username = username
|
|
||||||
this.password = passwordHash.crypt(password)
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
|
||||||
launchIo {
|
|
||||||
db.useTransaction {
|
|
||||||
db.users.add(newUser)
|
|
||||||
newUser
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: SQLIntegrityConstraintViolationException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun delete(userId: Int): Boolean = launchIo {
|
|
||||||
val updateCount = db.useTransaction {
|
|
||||||
db.delete(Users) { it.id eq userId }
|
|
||||||
}
|
|
||||||
when (updateCount) {
|
|
||||||
1 -> true
|
|
||||||
0 -> false
|
|
||||||
else -> error("??")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package be.vandewalleh.tables
|
|
||||||
|
|
||||||
import be.vandewalleh.entities.Note
|
|
||||||
import be.vandewalleh.extensions.uuidBinary
|
|
||||||
import me.liuwj.ktorm.schema.*
|
|
||||||
|
|
||||||
open class Notes(alias: String?) : Table<Note>("Notes", alias) {
|
|
||||||
companion object : Notes(null)
|
|
||||||
|
|
||||||
override fun aliased(alias: String) = Notes(alias)
|
|
||||||
|
|
||||||
val uuid = uuidBinary("uuid").primaryKey().bindTo { it.uuid }
|
|
||||||
val title = varchar("title").bindTo { it.title }
|
|
||||||
val content = varchar("content").bindTo { it.content }
|
|
||||||
val userId = int("user_id").references(Users) { it.user }
|
|
||||||
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
|
|
||||||
val user get() = userId.referenceTable as Users
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package be.vandewalleh.tables
|
|
||||||
|
|
||||||
import be.vandewalleh.entities.Tag
|
|
||||||
import be.vandewalleh.extensions.uuidBinary
|
|
||||||
import me.liuwj.ktorm.schema.*
|
|
||||||
|
|
||||||
open class Tags(alias: String?) : Table<Tag>("Tags", alias) {
|
|
||||||
companion object : Tags(null)
|
|
||||||
|
|
||||||
override fun aliased(alias: String) = Tags(alias)
|
|
||||||
|
|
||||||
val id = int("id").primaryKey().bindTo { it.id }
|
|
||||||
val name = varchar("name").bindTo { it.name }
|
|
||||||
val noteUuid = uuidBinary("note_uuid").references(Notes) { it.note }
|
|
||||||
val note get() = noteUuid.referenceTable as Notes
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package be.vandewalleh.tables
|
|
||||||
|
|
||||||
import be.vandewalleh.entities.User
|
|
||||||
import me.liuwj.ktorm.schema.*
|
|
||||||
|
|
||||||
open class Users(alias: String?) : Table<User>("Users", alias) {
|
|
||||||
companion object : Users(null)
|
|
||||||
|
|
||||||
override fun aliased(alias: String) = Users(alias)
|
|
||||||
|
|
||||||
val id = int("id").primaryKey().bindTo { it.id }
|
|
||||||
val username = varchar("username").bindTo { it.username }
|
|
||||||
val password = varchar("password").bindTo { it.password }
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package be.vandewalleh.validation
|
|
||||||
|
|
||||||
import am.ik.yavi.builder.ValidatorBuilder
|
|
||||||
import am.ik.yavi.builder.konstraint
|
|
||||||
import am.ik.yavi.core.Validator
|
|
||||||
import be.vandewalleh.entities.Note
|
|
||||||
|
|
||||||
val noteValidator: Validator<Note> = ValidatorBuilder.of<Note>()
|
|
||||||
.konstraint(Note::title) {
|
|
||||||
notNull().notBlank().lessThanOrEqual(50)
|
|
||||||
}
|
|
||||||
.konstraint(Note::tags) {
|
|
||||||
lessThanOrEqual(10)
|
|
||||||
}
|
|
||||||
.konstraint(Note::content) {
|
|
||||||
notNull().notBlank()
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package be.vandewalleh.validation
|
|
||||||
|
|
||||||
import am.ik.yavi.builder.ValidatorBuilder
|
|
||||||
import am.ik.yavi.builder.konstraint
|
|
||||||
import am.ik.yavi.core.Validator
|
|
||||||
import be.vandewalleh.entities.User
|
|
||||||
|
|
||||||
val registerValidator: Validator<User> = ValidatorBuilder.of<User>()
|
|
||||||
.konstraint(User::username) {
|
|
||||||
notNull().lessThanOrEqual(50).greaterThanOrEqual(3)
|
|
||||||
}
|
|
||||||
.konstraint(User::password) {
|
|
||||||
notNull().greaterThanOrEqual(6)
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package be.vandewalleh.validation
|
|
||||||
|
|
||||||
import am.ik.yavi.core.Validator
|
|
||||||
import be.vandewalleh.features.ValidationException
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.request.*
|
|
||||||
|
|
||||||
suspend inline fun <reified T : Any> ApplicationCall.receiveValidated(validator: Validator<T>): T {
|
|
||||||
val value: T = receive()
|
|
||||||
validator.validate(value).throwIfInvalid { ValidationException(it.details()[0].defaultMessage) }
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
package integration.routing
|
|
||||||
|
|
||||||
import be.vandewalleh.Config
|
|
||||||
import be.vandewalleh.auth.SimpleJWT
|
|
||||||
import be.vandewalleh.entities.User
|
|
||||||
import be.vandewalleh.features.PasswordHash
|
|
||||||
import be.vandewalleh.mainModule
|
|
||||||
import be.vandewalleh.module
|
|
||||||
import be.vandewalleh.services.UserService
|
|
||||||
import com.auth0.jwt.JWT
|
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.server.testing.*
|
|
||||||
import io.mockk.coEvery
|
|
||||||
import io.mockk.coVerify
|
|
||||||
import io.mockk.mockk
|
|
||||||
import org.amshove.kluent.*
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.junit.jupiter.api.*
|
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.kodein.di.bind
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import utils.*
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
|
||||||
class AuthControllerKtTest {
|
|
||||||
|
|
||||||
private val userService = mockk<UserService>()
|
|
||||||
|
|
||||||
private val kodein = DI {
|
|
||||||
import(mainModule, allowOverride = true)
|
|
||||||
bind<UserService>(overrides = true) with instance(userService)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val passwordHash by kodein.instance<PasswordHash>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
|
|
||||||
val user = User {
|
|
||||||
password = passwordHash.crypt("password")
|
|
||||||
username = "existing"
|
|
||||||
}
|
|
||||||
user["id"] = 1
|
|
||||||
|
|
||||||
coEvery { userService.find("existing") } returns user
|
|
||||||
coEvery { userService.exists(1) } returns true
|
|
||||||
coEvery { userService.find(1) } returns User {
|
|
||||||
username = "existing"
|
|
||||||
}
|
|
||||||
|
|
||||||
val user2 = User {
|
|
||||||
password = passwordHash.crypt("right password")
|
|
||||||
username = "wrong"
|
|
||||||
}
|
|
||||||
user["id"] = 2
|
|
||||||
coEvery { userService.find("wrong") } returns user2
|
|
||||||
|
|
||||||
coEvery { userService.find("notExisting") } returns null
|
|
||||||
|
|
||||||
coEvery { userService.exists(3) } returns false
|
|
||||||
coEvery { userService.find(3) } returns null
|
|
||||||
}
|
|
||||||
|
|
||||||
private val testEngine = TestApplicationEngine().apply {
|
|
||||||
start()
|
|
||||||
application.module(kodein)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class Login {
|
|
||||||
@Test
|
|
||||||
fun `login existing user with valid password`() {
|
|
||||||
val res = testEngine.post("/user/login") {
|
|
||||||
json {
|
|
||||||
it["username"] = "existing"
|
|
||||||
it["password"] = "password"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coVerify { userService.find("existing") }
|
|
||||||
|
|
||||||
res.status() `should be equal to` HttpStatusCode.OK
|
|
||||||
val jsonObject = JSONObject(res.content)
|
|
||||||
|
|
||||||
val hasToken = jsonObject.has("token")
|
|
||||||
hasToken `should be equal to` true
|
|
||||||
|
|
||||||
jsonObject.keyList() `should be equal to` listOf("token", "refreshToken")
|
|
||||||
|
|
||||||
val authJwt by kodein.instance<SimpleJWT>(tag = "auth")
|
|
||||||
val token = jsonObject.getString("token")
|
|
||||||
authJwt.verifier.verify(token)
|
|
||||||
|
|
||||||
val refreshJwt by kodein.instance<SimpleJWT>(tag = "refresh")
|
|
||||||
val refreshToken = jsonObject.getString("refreshToken")
|
|
||||||
refreshJwt.verifier.verify(refreshToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `login existing user with invalid password`() {
|
|
||||||
val res = testEngine.post("/user/login") {
|
|
||||||
json {
|
|
||||||
it["username"] = "wrong"
|
|
||||||
it["password"] = "not this"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coVerify { userService.find("wrong") }
|
|
||||||
|
|
||||||
res.status() `should be equal to` HttpStatusCode.Unauthorized
|
|
||||||
res.content `should strictly be equal to json` """{msg: "Unauthorized"}"""
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `login not existing user`() {
|
|
||||||
val res = testEngine.post("/user/login") {
|
|
||||||
json {
|
|
||||||
it["username"] = "notExisting"
|
|
||||||
it["password"] = "babababa"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coVerify { userService.find("notExisting") }
|
|
||||||
|
|
||||||
res.status() `should be equal to` HttpStatusCode.Unauthorized
|
|
||||||
res.content `should strictly be equal to json` """{msg: "Unauthorized"}"""
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `login without body`() {
|
|
||||||
val res = testEngine.post("/user/login") {
|
|
||||||
addHeader(HttpHeaders.ContentType, "application/json")
|
|
||||||
}
|
|
||||||
res.status() `should be equal to` HttpStatusCode.BadRequest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class Refresh {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test valid refresh token`() {
|
|
||||||
val refreshJwt by kodein.instance<SimpleJWT>(tag = "refresh")
|
|
||||||
val refreshToken = refreshJwt.sign(1)
|
|
||||||
|
|
||||||
val res = testEngine.post("/user/refresh_token") {
|
|
||||||
json {
|
|
||||||
it["refreshToken"] = refreshToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val jsonObject = JSONObject(res.content)
|
|
||||||
jsonObject.keyList() `should be equal to` listOf("token", "refreshToken")
|
|
||||||
|
|
||||||
coVerify { userService.exists(1) }
|
|
||||||
res.status() `should be equal to` HttpStatusCode.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test valid refresh token for deleted user`() {
|
|
||||||
val refreshJwt by kodein.instance<SimpleJWT>(tag = "refresh")
|
|
||||||
val refreshToken = refreshJwt.sign(3)
|
|
||||||
|
|
||||||
val res = testEngine.post("/user/refresh_token") {
|
|
||||||
json {
|
|
||||||
it["refreshToken"] = refreshToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coVerify { userService.exists(3) }
|
|
||||||
res.status() `should be equal to` HttpStatusCode.Unauthorized
|
|
||||||
res.content `should strictly be equal to json` """{msg: "Unauthorized"}"""
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test expired refresh token for existing user`() {
|
|
||||||
val config by kodein.instance<Config>()
|
|
||||||
val algorithm = Algorithm.HMAC256(config.jwt.refresh.secret.value)
|
|
||||||
|
|
||||||
val expiredToken = JWT.create()
|
|
||||||
.withClaim("id", 1)
|
|
||||||
.withExpiresAt(Date(0)) // January 1, 1970, 00:00:00 GMT
|
|
||||||
.sign(algorithm)
|
|
||||||
|
|
||||||
val res = testEngine.post("/user/refresh_token") {
|
|
||||||
json {
|
|
||||||
it["refreshToken"] = expiredToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status() `should be equal to` HttpStatusCode.Unauthorized
|
|
||||||
res.content `should strictly be equal to json` """{msg: "Unauthorized"}"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class UserInfo {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test user info for existing user`() {
|
|
||||||
val authJwt by kodein.instance<SimpleJWT>(tag = "auth")
|
|
||||||
val token = authJwt.sign(1)
|
|
||||||
val res = testEngine.get("/user/me") {
|
|
||||||
setToken(token)
|
|
||||||
}
|
|
||||||
res.content `should strictly be equal to json` """{user:{username:"existing"}}"""
|
|
||||||
res.status() `should be equal to` HttpStatusCode.OK
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test user info on deleted user`() {
|
|
||||||
val authJwt by kodein.instance<SimpleJWT>(tag = "auth")
|
|
||||||
val token = authJwt.sign(3)
|
|
||||||
val res = testEngine.get("/user/me") {
|
|
||||||
setToken(token)
|
|
||||||
}
|
|
||||||
res.status()!!.value `should not be in range` (200..299)
|
|
||||||
val jsonObject = JSONObject(res.content)
|
|
||||||
jsonObject.keyList() `should be equal to` listOf("msg")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package integration.routing
|
|
||||||
|
|
||||||
import be.vandewalleh.auth.SimpleJWT
|
|
||||||
import be.vandewalleh.entities.User
|
|
||||||
import be.vandewalleh.mainModule
|
|
||||||
import be.vandewalleh.module
|
|
||||||
import be.vandewalleh.services.UserService
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.server.testing.*
|
|
||||||
import io.mockk.coEvery
|
|
||||||
import io.mockk.mockk
|
|
||||||
import org.amshove.kluent.*
|
|
||||||
import org.junit.jupiter.api.*
|
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.kodein.di.bind
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import utils.*
|
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
|
||||||
class UserControllerKtTest {
|
|
||||||
|
|
||||||
private val userService = mockk<UserService>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
// new user
|
|
||||||
coEvery { userService.exists("new") } returns false
|
|
||||||
coEvery { userService.create("new", any()) } returns User {
|
|
||||||
this.username = "new"
|
|
||||||
}
|
|
||||||
|
|
||||||
// existing user
|
|
||||||
coEvery { userService.exists("existing") } returns true
|
|
||||||
coEvery { userService.create("existing", any()) } returns null
|
|
||||||
coEvery { userService.delete(1) } returns true andThen false
|
|
||||||
|
|
||||||
// modified user
|
|
||||||
coEvery { userService.exists("modified") } returns true
|
|
||||||
coEvery { userService.exists(and(not("modified"), not("existing"))) } returns false
|
|
||||||
coEvery { userService.exists(1) } returns true
|
|
||||||
coEvery { userService.create("modified", any()) } returns null
|
|
||||||
}
|
|
||||||
|
|
||||||
private val kodein = DI {
|
|
||||||
import(mainModule, allowOverride = true)
|
|
||||||
bind<UserService>(overrides = true) with instance(userService)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val testEngine = TestApplicationEngine().apply {
|
|
||||||
start()
|
|
||||||
application.module(kodein)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class CreateUser {
|
|
||||||
@Test
|
|
||||||
fun `create a new user`() {
|
|
||||||
val res = testEngine.post("/user") {
|
|
||||||
json {
|
|
||||||
it["username"] = "new"
|
|
||||||
it["password"] = "test123abc"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.status() `should be equal to` HttpStatusCode.Created
|
|
||||||
res.content `should strictly be equal to json` """{username:"new"}"""
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `create an existing user`() {
|
|
||||||
val res = testEngine.post("/user") {
|
|
||||||
json {
|
|
||||||
it["username"] = "existing"
|
|
||||||
it["password"] = "test123abc"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.status() `should be equal to` HttpStatusCode.Conflict
|
|
||||||
res.content `should be equal to json` """{msg:"Conflict"}"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class DeleteUser {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `delete an existing user`() {
|
|
||||||
val authJwt by kodein.instance<SimpleJWT>("auth")
|
|
||||||
val token = authJwt.sign(1)
|
|
||||||
|
|
||||||
val res = testEngine.delete("/user") {
|
|
||||||
addHeader(HttpHeaders.Authorization, "Bearer $token")
|
|
||||||
}
|
|
||||||
res.status() `should be equal to` HttpStatusCode.OK
|
|
||||||
res.content `should be equal to json` """{msg:"OK"}"""
|
|
||||||
|
|
||||||
// try again
|
|
||||||
val res2 = testEngine.delete("/user") {
|
|
||||||
setToken(token)
|
|
||||||
}
|
|
||||||
res2.status() `should be equal to` HttpStatusCode.NotFound
|
|
||||||
res2.content `should be equal to json` """{msg:"Not Found"}"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
package integration.services
|
|
||||||
|
|
||||||
import am.ik.yavi.builder.ValidatorBuilder
|
|
||||||
import am.ik.yavi.core.CustomConstraint
|
|
||||||
import am.ik.yavi.core.Validator
|
|
||||||
import be.vandewalleh.entities.Note
|
|
||||||
import be.vandewalleh.features.Migration
|
|
||||||
import be.vandewalleh.mainModule
|
|
||||||
import be.vandewalleh.services.NoteService
|
|
||||||
import be.vandewalleh.services.UserService
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.databind.util.StdDateFormat
|
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import com.github.javafaker.Faker
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import me.liuwj.ktorm.jackson.*
|
|
||||||
import org.amshove.kluent.*
|
|
||||||
import org.junit.jupiter.api.*
|
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.kodein.di.bind
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import org.kodein.di.singleton
|
|
||||||
import utils.KMariadbContainer
|
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
|
||||||
class NoteServiceTest {
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class DB {
|
|
||||||
private val mariadb = KMariadbContainer().apply { start() }
|
|
||||||
|
|
||||||
private val kodein = DI {
|
|
||||||
import(mainModule, allowOverride = true)
|
|
||||||
bind(overrides = true) from singleton { mariadb.datasource() }
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
val migration by kodein.instance<Migration>()
|
|
||||||
migration.migrate()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun run() {
|
|
||||||
val userService by kodein.instance<UserService>()
|
|
||||||
val user = runBlocking { userService.create("test", "test")!! }
|
|
||||||
val noteService by kodein.instance<NoteService>()
|
|
||||||
val note = runBlocking {
|
|
||||||
noteService.create(
|
|
||||||
user.id,
|
|
||||||
Note {
|
|
||||||
this.title = "a note"
|
|
||||||
this.content =
|
|
||||||
"""
|
|
||||||
|# Title
|
|
||||||
|
|
|
||||||
|😝😝😝😝
|
|
||||||
|another line
|
|
||||||
""".trimMargin()
|
|
||||||
this.tags = listOf("a", "tag")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
println(note)
|
|
||||||
|
|
||||||
val objectMapper = ObjectMapper().apply {
|
|
||||||
registerModule(JavaTimeModule())
|
|
||||||
registerModule(KtormModule())
|
|
||||||
disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT)
|
|
||||||
dateFormat = StdDateFormat()
|
|
||||||
}
|
|
||||||
val json = objectMapper.writeValueAsString(note)
|
|
||||||
println(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test tag list`() {
|
|
||||||
val userService by kodein.instance<UserService>()
|
|
||||||
val user = runBlocking { userService.create("test", "test")!! }
|
|
||||||
val user2 = runBlocking { userService.create("user2", "test")!! }
|
|
||||||
|
|
||||||
val noteService by kodein.instance<NoteService>()
|
|
||||||
runBlocking {
|
|
||||||
noteService.create(
|
|
||||||
user.id,
|
|
||||||
Note {
|
|
||||||
title = "test"
|
|
||||||
content = ""
|
|
||||||
tags = listOf("same")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
noteService.create(
|
|
||||||
user.id,
|
|
||||||
Note {
|
|
||||||
title = "test2"
|
|
||||||
content = ""
|
|
||||||
tags = listOf("same")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
noteService.create(
|
|
||||||
user.id,
|
|
||||||
Note {
|
|
||||||
title = "test3"
|
|
||||||
content = ""
|
|
||||||
tags = listOf("another")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
noteService.create(
|
|
||||||
user2.id,
|
|
||||||
Note {
|
|
||||||
title = "test"
|
|
||||||
content = ""
|
|
||||||
tags = listOf("user2")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val user1Tags = runBlocking { noteService.getTags(user.id) }
|
|
||||||
user1Tags `should be equal to` listOf("same", "another")
|
|
||||||
val user2Tags = runBlocking { noteService.getTags(user2.id) }
|
|
||||||
user2Tags `should be equal to` listOf("user2")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test patch note`() {
|
|
||||||
val noteService by kodein.instance<NoteService>()
|
|
||||||
val userService by kodein.instance<UserService>()
|
|
||||||
val user = runBlocking { userService.create(Faker().name().username(), "test") }!!
|
|
||||||
val note = runBlocking {
|
|
||||||
noteService.create(
|
|
||||||
user.id,
|
|
||||||
Note {
|
|
||||||
this.title = "title"
|
|
||||||
this.content = "old content"
|
|
||||||
this.tags = emptyList()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val get = runBlocking { noteService.find(user.id, note.uuid) }
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
noteService.updateNote(
|
|
||||||
user.id,
|
|
||||||
Note {
|
|
||||||
uuid = note.uuid
|
|
||||||
title = "new title"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val updated = runBlocking { noteService.find(user.id, note.uuid) }
|
|
||||||
println("updated: $updated")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class NoteValidation {
|
|
||||||
@Test
|
|
||||||
fun `test update constraints`() {
|
|
||||||
|
|
||||||
val fieldPresentConstraint = object : CustomConstraint<Note> {
|
|
||||||
override fun defaultMessageFormat() = "fmt {0} {1} {2}"
|
|
||||||
|
|
||||||
override fun messageKey() = "title|content|tags"
|
|
||||||
|
|
||||||
override fun test(note: Note): Boolean {
|
|
||||||
val hasTitle = note["title"] != null
|
|
||||||
val hasContent = note["content"] != null
|
|
||||||
val hasTags = note["tags"] != null && note.tags.isNotEmpty()
|
|
||||||
return hasTitle || hasContent || hasTags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val userValidator: Validator<Note> = ValidatorBuilder<Note>()
|
|
||||||
.constraintOnTarget(fieldPresentConstraint, "present")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
userValidator.validate(
|
|
||||||
Note {
|
|
||||||
title = "this is a title"
|
|
||||||
}
|
|
||||||
).isValid `should be equal to` true
|
|
||||||
|
|
||||||
userValidator.validate(
|
|
||||||
Note {
|
|
||||||
content = "this is a title"
|
|
||||||
}
|
|
||||||
).isValid `should be equal to` true
|
|
||||||
|
|
||||||
userValidator.validate(
|
|
||||||
Note {
|
|
||||||
tags = emptyList()
|
|
||||||
}
|
|
||||||
).isValid `should be equal to` false
|
|
||||||
|
|
||||||
userValidator.validate(
|
|
||||||
Note {
|
|
||||||
tags = listOf("tags")
|
|
||||||
}
|
|
||||||
).isValid `should be equal to` true
|
|
||||||
|
|
||||||
userValidator.validate(
|
|
||||||
Note {
|
|
||||||
tags = listOf("tags")
|
|
||||||
title = "This is a title"
|
|
||||||
}
|
|
||||||
).isValid `should be equal to` true
|
|
||||||
|
|
||||||
userValidator.validate(
|
|
||||||
Note {
|
|
||||||
tags = listOf("tags")
|
|
||||||
title = "This is a title"
|
|
||||||
content =
|
|
||||||
"""
|
|
||||||
|# This is
|
|
||||||
|
|
|
||||||
|some markdown content
|
|
||||||
""".trimMargin()
|
|
||||||
}
|
|
||||||
).isValid `should be equal to` true
|
|
||||||
|
|
||||||
userValidator.validate(
|
|
||||||
Note {
|
|
||||||
tags = listOf("tags")
|
|
||||||
title = "This is a title"
|
|
||||||
content =
|
|
||||||
"""
|
|
||||||
|# This is
|
|
||||||
|
|
|
||||||
|some markdown content
|
|
||||||
""".trimMargin()
|
|
||||||
}
|
|
||||||
).isValid `should be equal to` true
|
|
||||||
|
|
||||||
userValidator.validate(Note()).isValid `should be equal to` false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class NoteEntity {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test entity`() {
|
|
||||||
val objectMapper = ObjectMapper().apply {
|
|
||||||
registerModule(JavaTimeModule())
|
|
||||||
registerModule(KtormModule())
|
|
||||||
disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT)
|
|
||||||
dateFormat = StdDateFormat()
|
|
||||||
}
|
|
||||||
val note: Note = objectMapper.readValue("""{"uuid": "2007e4d7-2986-4188-bde1-b99916d94bad"}""")
|
|
||||||
println(note.uuid)
|
|
||||||
println(note.uuid::class.qualifiedName)
|
|
||||||
println(note.uuid.leastSignificantBits)
|
|
||||||
println(note.uuid.mostSignificantBits)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package integration.services
|
|
||||||
|
|
||||||
import be.vandewalleh.features.Migration
|
|
||||||
import be.vandewalleh.mainModule
|
|
||||||
import be.vandewalleh.services.UserService
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.amshove.kluent.*
|
|
||||||
import org.junit.jupiter.api.*
|
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.kodein.di.bind
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import org.kodein.di.singleton
|
|
||||||
import utils.KMariadbContainer
|
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
|
||||||
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
|
|
||||||
class UserServiceTest {
|
|
||||||
|
|
||||||
private val mariadb = KMariadbContainer().apply { start() }
|
|
||||||
|
|
||||||
private val kodein = DI {
|
|
||||||
import(mainModule, allowOverride = true)
|
|
||||||
bind(overrides = true) from singleton { mariadb.datasource() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private val userService by kodein.instance<UserService>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
val migration by kodein.instance<Migration>()
|
|
||||||
migration.migrate()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(1)
|
|
||||||
fun `test create user`() {
|
|
||||||
runBlocking {
|
|
||||||
val username = "hubert"
|
|
||||||
val password = "password"
|
|
||||||
|
|
||||||
userService.create(username, password)
|
|
||||||
val user = userService.find(username)
|
|
||||||
user `should not be` null
|
|
||||||
user?.username `should be equal to` username
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(2)
|
|
||||||
fun `test create same user`() {
|
|
||||||
runBlocking {
|
|
||||||
userService.create(username = "hubert", password = "password") `should be` null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(3)
|
|
||||||
fun `test delete user`() {
|
|
||||||
runBlocking {
|
|
||||||
val id = userService.find("hubert")!!.id
|
|
||||||
userService.delete(id)
|
|
||||||
|
|
||||||
userService.find("hubert") `should be` null
|
|
||||||
userService.find(id) `should be` null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package unit.validation
|
|
||||||
|
|
||||||
import be.vandewalleh.entities.User
|
|
||||||
import be.vandewalleh.validation.registerValidator
|
|
||||||
import org.amshove.kluent.*
|
|
||||||
import org.junit.jupiter.api.*
|
|
||||||
import utils.firstInvalid
|
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
|
||||||
class RegisterValidationTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `valid register test`() {
|
|
||||||
val violations = registerValidator.validate(
|
|
||||||
User {
|
|
||||||
username = "hubert"
|
|
||||||
password = "definitelyNotMyPassword"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
violations.isValid `should be equal to` true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `username too long test`() {
|
|
||||||
val violations = registerValidator.validate(
|
|
||||||
User {
|
|
||||||
username = "6X9iboWmEOWjVjkO328ReTJ1gGPTTmB/ZGgBLhB6EzAJoWkJht8"
|
|
||||||
password = "definitelyNotMyPassword"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
violations.isValid `should be equal to` false
|
|
||||||
violations.firstInvalid `should be equal to` "username"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import org.skyscreamer.jsonassert.JSONAssert
|
|
||||||
|
|
||||||
infix fun String?.shouldBeEqualToJson(expected: String?) = JSONAssert.assertEquals(expected, this, false)
|
|
||||||
|
|
||||||
infix fun String?.`should be equal to json`(expected: String?) = shouldBeEqualToJson(expected)
|
|
||||||
|
|
||||||
infix fun String?.shouldStrictlyBeEqualToJson(expected: String?) = JSONAssert.assertEquals(expected, this, true)
|
|
||||||
|
|
||||||
infix fun String?.`should strictly be equal to json`(expected: String?) = shouldStrictlyBeEqualToJson(expected)
|
|
||||||
|
|
||||||
infix fun String?.shouldNotStrictlyBeEqualToJson(expected: String?) = JSONAssert.assertNotEquals(expected, this, true)
|
|
||||||
|
|
||||||
infix fun String?.`should not strictly be equal to json`(expected: String?) = shouldNotStrictlyBeEqualToJson(expected)
|
|
||||||
|
|
||||||
infix fun String?.shouldNotBeEqualToJson(expected: String?) = JSONAssert.assertNotEquals(expected, this, false)
|
|
||||||
|
|
||||||
infix fun String?.`should not be equal to json`(expected: String?) = shouldNotBeEqualToJson(expected)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
operator fun JSONObject.set(name: String, value: String) {
|
|
||||||
this.put(name, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun JSONObject.set(name: String, value: Double) {
|
|
||||||
this.put(name, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun JSONObject.set(name: String, value: Long) {
|
|
||||||
this.put(name, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun JSONObject.set(name: String, value: Int) {
|
|
||||||
this.put(name, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun JSONObject.set(name: String, value: Boolean) {
|
|
||||||
this.put(name, value)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import com.zaxxer.hikari.HikariConfig
|
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
|
||||||
import org.testcontainers.containers.MariaDBContainer
|
|
||||||
|
|
||||||
class KMariadbContainer : MariaDBContainer<KMariadbContainer>() {
|
|
||||||
fun datasource(): HikariDataSource {
|
|
||||||
val hikariConfig = HikariConfig().apply {
|
|
||||||
jdbcUrl = this@KMariadbContainer.jdbcUrl
|
|
||||||
username = this@KMariadbContainer.username
|
|
||||||
password = this@KMariadbContainer.password
|
|
||||||
}
|
|
||||||
|
|
||||||
return HikariDataSource(hikariConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import io.ktor.http.HttpHeaders
|
|
||||||
import io.ktor.http.HttpMethod
|
|
||||||
import io.ktor.server.testing.*
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
fun TestApplicationRequest.json(block: (JSONObject) -> Unit) {
|
|
||||||
addHeader(HttpHeaders.ContentType, "application/json")
|
|
||||||
setBody(JSONObject().apply(block).toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun TestApplicationRequest.setToken(token: String) {
|
|
||||||
addHeader(HttpHeaders.Authorization, "Bearer $token")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun TestApplicationEngine.post(
|
|
||||||
uri: String,
|
|
||||||
setup: TestApplicationRequest.() -> Unit = {}
|
|
||||||
): TestApplicationResponse = handleRequest {
|
|
||||||
this.uri = uri
|
|
||||||
this.method = HttpMethod.Post
|
|
||||||
setup()
|
|
||||||
}.response
|
|
||||||
|
|
||||||
fun TestApplicationEngine.get(
|
|
||||||
uri: String,
|
|
||||||
setup: TestApplicationRequest.() -> Unit = {}
|
|
||||||
): TestApplicationResponse = handleRequest {
|
|
||||||
this.uri = uri
|
|
||||||
this.method = HttpMethod.Get
|
|
||||||
setup()
|
|
||||||
}.response
|
|
||||||
|
|
||||||
fun TestApplicationEngine.delete(
|
|
||||||
uri: String,
|
|
||||||
setup: TestApplicationRequest.() -> Unit = {}
|
|
||||||
): TestApplicationResponse = handleRequest {
|
|
||||||
this.uri = uri
|
|
||||||
this.method = HttpMethod.Delete
|
|
||||||
setup()
|
|
||||||
}.response
|
|
||||||
|
|
||||||
fun TestApplicationEngine.put(
|
|
||||||
uri: String,
|
|
||||||
setup: TestApplicationRequest.() -> Unit = {}
|
|
||||||
): TestApplicationResponse = handleRequest {
|
|
||||||
this.uri = uri
|
|
||||||
this.method = HttpMethod.Put
|
|
||||||
setup()
|
|
||||||
}.response
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
fun JSONObject.keyList(): List<Any?> = keys().asSequence().toList()
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import am.ik.yavi.core.ConstraintViolations
|
|
||||||
|
|
||||||
val ConstraintViolations.firstInvalid: Any?
|
|
||||||
get() = this.violations().firstOrNull()?.name()
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import be.simplenotes.Libs
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("be.simplenotes.base")
|
||||||
|
id("be.simplenotes.kotlinx-serialization")
|
||||||
|
id("be.simplenotes.app-shadow")
|
||||||
|
id("be.simplenotes.app-css")
|
||||||
|
id("be.simplenotes.app-docker")
|
||||||
|
kotlin("kapt")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":domain"))
|
||||||
|
implementation(project(":types"))
|
||||||
|
implementation(project(":config"))
|
||||||
|
implementation(project(":views"))
|
||||||
|
|
||||||
|
implementation(Libs.arrowCoreData)
|
||||||
|
implementation(Libs.konform)
|
||||||
|
implementation(Libs.http4kCore)
|
||||||
|
implementation(Libs.jettyServer)
|
||||||
|
implementation(Libs.jettyServlet)
|
||||||
|
implementation(Libs.javaxServlet)
|
||||||
|
implementation(Libs.kotlinxSerializationJson)
|
||||||
|
implementation(Libs.logbackClassic)
|
||||||
|
|
||||||
|
implementation(Libs.micronaut)
|
||||||
|
kapt(Libs.micronautProcessor)
|
||||||
|
|
||||||
|
testImplementation(Libs.junit)
|
||||||
|
testImplementation(Libs.assertJ)
|
||||||
|
testImplementation(Libs.http4kTestingHamkrest)
|
||||||
|
}
|
||||||
@@ -2,19 +2,17 @@
|
|||||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<withJansi>true</withJansi>
|
<withJansi>true</withJansi>
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>%cyan(%d{YYYY-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %magenta(%logger{36}) - %msg%n
|
<pattern>%cyan(%d{YYYY-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %green(%logger{36}) - %msg%n
|
||||||
</pattern>
|
</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
<root level="INFO">
|
<root level="DEBUG">
|
||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT"/>
|
||||||
</root>
|
</root>
|
||||||
<logger name="me.liuwj.ktorm.database" level="DEBUG"/>
|
|
||||||
<logger name="com.zaxxer.hikari" level="INFO"/>
|
|
||||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||||
<logger name="io.netty" level="INFO"/>
|
<logger name="me.liuwj.ktorm.database" level="INFO"/>
|
||||||
|
<logger name="com.zaxxer.hikari" level="INFO"/>
|
||||||
<logger name="org.flywaydb.core" level="INFO"/>
|
<logger name="org.flywaydb.core" level="INFO"/>
|
||||||
<logger name="org.testcontainers" level="INFO"/>
|
<logger name="io.micronaut" level="INFO"/>
|
||||||
<logger name="com.github.dockerjava" level="WARN"/>
|
<logger name="io.micronaut.context.lifecycle" level="INFO"/>
|
||||||
<logger name="🐳 [mariadb:10.3.6]" level="WARN"/>
|
|
||||||
</configuration>
|
</configuration>
|
||||||
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#00aba9</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
|
After Width: | Height: | Size: 814 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('#note pre code').forEach((b) => {
|
||||||
|
hljs.highlightBlock(b);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /$
|
||||||
|
Disallow: /
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M1255 6993 c-179 -23 -313 -62 -461 -133 -396 -187 -665 -533 -766
|
||||||
|
-981 l-23 -104 0 -2275 0 -2275 23 -102 c125 -565 530 -970 1095 -1095 l102
|
||||||
|
-23 2275 0 2275 0 102 23 c565 125 970 530 1095 1095 l23 102 0 2275 0 2275
|
||||||
|
-23 102 c-125 566 -521 964 -1090 1095 l-97 22 -2250 2 c-1237 1 -2263 -1
|
||||||
|
-2280 -3z m1024 -1979 c128 -18 287 -70 394 -127 262 -139 448 -395 472 -649
|
||||||
|
3 -34 8 -78 11 -95 l5 -33 -255 0 -254 0 -6 28 c-2 15 -7 44 -11 65 -27 168
|
||||||
|
-204 335 -416 393 -94 26 -317 24 -421 -4 -218 -59 -345 -196 -356 -384 -6
|
||||||
|
-105 16 -173 80 -243 81 -88 227 -148 563 -230 509 -123 742 -228 916 -412
|
||||||
|
123 -131 170 -248 176 -448 9 -245 -55 -420 -212 -580 -162 -165 -387 -270
|
||||||
|
-667 -311 -154 -22 -461 -15 -595 15 -280 62 -513 193 -662 373 -72 85 -162
|
||||||
|
262 -185 358 -8 36 -18 95 -22 133 l-6 67 254 0 255 0 6 -53 c22 -179 135
|
||||||
|
-332 310 -417 127 -61 195 -74 387 -75 195 0 279 16 399 76 179 89 260 234
|
||||||
|
231 415 -22 137 -98 231 -243 302 -109 55 -202 84 -447 142 -237 57 -306 76
|
||||||
|
-427 120 -340 125 -535 303 -600 550 -23 87 -23 271 0 360 87 335 409 595 827
|
||||||
|
665 108 19 371 18 499 -1z m3170 0 c202 -35 325 -95 453 -224 77 -77 93 -100
|
||||||
|
141 -201 30 -63 63 -143 72 -179 49 -183 48 -159 52 -1307 l4 -1083 -256 0
|
||||||
|
-255 0 0 1034 c0 1160 1 1133 -70 1282 -92 192 -259 271 -548 262 -117 -4
|
||||||
|
-149 -9 -221 -33 -177 -60 -340 -200 -440 -376 l-31 -56 0 -1056 0 -1057 -255
|
||||||
|
0 -255 0 0 1480 0 1480 239 0 238 0 6 -82 c4 -46 7 -120 7 -165 0 -45 3 -88 6
|
||||||
|
-97 4 -11 28 8 92 72 163 164 356 267 575 307 103 19 334 18 446 -1z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "SimpleNotes",
|
||||||
|
"short_name": "SimpleNotes",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package be.simplenotes.app
|
||||||
|
|
||||||
|
import org.http4k.server.Http4kServer
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import javax.annotation.PostConstruct
|
||||||
|
import javax.annotation.PreDestroy
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class Server(
|
||||||
|
private val config: SimpleNotesServerConfig,
|
||||||
|
private val http4kServer: Http4kServer,
|
||||||
|
) {
|
||||||
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
fun start(): Server {
|
||||||
|
http4kServer.start()
|
||||||
|
logger.info("Listening on http://${config.host}:${http4kServer.port()}")
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
fun stop() {
|
||||||
|
logger.info("Stopping server")
|
||||||
|
http4kServer.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package be.simplenotes.app
|
||||||
|
|
||||||
|
import io.micronaut.context.ApplicationContext
|
||||||
|
import java.lang.Runtime.getRuntime
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val ctx = ApplicationContext.run()
|
||||||
|
ctx.createBean(Server::class.java)
|
||||||
|
getRuntime().addShutdownHook(Thread { ctx.stop() })
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package be.simplenotes.app.api
|
||||||
|
|
||||||
|
import be.simplenotes.app.extensions.auto
|
||||||
|
import be.simplenotes.domain.usecases.NoteService
|
||||||
|
import be.simplenotes.types.LoggedInUser
|
||||||
|
import be.simplenotes.types.PersistedNote
|
||||||
|
import be.simplenotes.types.PersistedNoteMetadata
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.http4k.core.Request
|
||||||
|
import org.http4k.core.Response
|
||||||
|
import org.http4k.core.Status.Companion.BAD_REQUEST
|
||||||
|
import org.http4k.core.Status.Companion.NOT_FOUND
|
||||||
|
import org.http4k.core.Status.Companion.OK
|
||||||
|
import org.http4k.lens.Path
|
||||||
|
import org.http4k.lens.uuid
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ApiNoteController(
|
||||||
|
json: Json,
|
||||||
|
private val noteService: NoteService,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun createNote(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
val content = noteContentLens(request)
|
||||||
|
return noteService.create(loggedInUser, content).fold(
|
||||||
|
{ Response(BAD_REQUEST) },
|
||||||
|
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notes(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
val notes = noteService.paginatedNotes(loggedInUser.userId, page = 1).notes
|
||||||
|
return persistedNotesMetadataLens(notes, Response(OK))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun note(request: Request, loggedInUser: LoggedInUser): Response =
|
||||||
|
noteService.find(loggedInUser.userId, uuidLens(request))
|
||||||
|
?.let { persistedNoteLens(it, Response(OK)) }
|
||||||
|
?: Response(NOT_FOUND)
|
||||||
|
|
||||||
|
fun update(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
val content = noteContentLens(request)
|
||||||
|
return noteService.update(loggedInUser, uuidLens(request), content).fold(
|
||||||
|
{
|
||||||
|
Response(BAD_REQUEST)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
if (it == null) Response(NOT_FOUND)
|
||||||
|
else Response(OK)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
val query = searchContentLens(request)
|
||||||
|
val notes = noteService.search(loggedInUser.userId, query)
|
||||||
|
return persistedNotesMetadataLens(notes, Response(OK))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val uuidContentLens = json.auto<UuidContent>().toLens()
|
||||||
|
private val noteContentLens = json.auto<NoteContent>().map { it.content }.toLens()
|
||||||
|
private val searchContentLens = json.auto<SearchContent>().map { it.query }.toLens()
|
||||||
|
private val persistedNotesMetadataLens = json.auto<List<PersistedNoteMetadata>>().toLens()
|
||||||
|
private val persistedNoteLens = json.auto<PersistedNote>().toLens()
|
||||||
|
private val uuidLens = Path.uuid().of("uuid")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class NoteContent(val content: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UuidContent(@Contextual val uuid: UUID)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SearchContent(@Contextual val query: String)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package be.simplenotes.app.api
|
||||||
|
|
||||||
|
import be.simplenotes.app.extensions.auto
|
||||||
|
import be.simplenotes.domain.usecases.UserService
|
||||||
|
import be.simplenotes.domain.usecases.users.login.LoginForm
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.http4k.core.Request
|
||||||
|
import org.http4k.core.Response
|
||||||
|
import org.http4k.core.Status.Companion.BAD_REQUEST
|
||||||
|
import org.http4k.core.Status.Companion.OK
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ApiUserController(
|
||||||
|
json: Json,
|
||||||
|
private val userService: UserService,
|
||||||
|
) {
|
||||||
|
private val tokenLens = json.auto<Token>().toLens()
|
||||||
|
private val loginFormLens = json.auto<LoginForm>().toLens()
|
||||||
|
|
||||||
|
fun login(request: Request) = userService
|
||||||
|
.login(loginFormLens(request))
|
||||||
|
.fold(
|
||||||
|
{ Response(BAD_REQUEST) },
|
||||||
|
{ tokenLens(Token(it), Response(OK)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Token(val token: String)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package be.simplenotes.app.controllers
|
||||||
|
|
||||||
|
import be.simplenotes.app.extensions.html
|
||||||
|
import be.simplenotes.types.LoggedInUser
|
||||||
|
import be.simplenotes.views.BaseView
|
||||||
|
import org.http4k.core.Request
|
||||||
|
import org.http4k.core.Response
|
||||||
|
import org.http4k.core.Status.Companion.OK
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class BaseController(private val view: BaseView) {
|
||||||
|
fun index(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser?) =
|
||||||
|
Response(OK).html(view.renderHome(loggedInUser))
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package be.simplenotes.app.controllers
|
||||||
|
|
||||||
|
import be.simplenotes.domain.usecases.HealthCheckService
|
||||||
|
import org.http4k.core.Request
|
||||||
|
import org.http4k.core.Response
|
||||||
|
import org.http4k.core.Status.Companion.OK
|
||||||
|
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class HealthCheckController(private val healthCheckService: HealthCheckService) {
|
||||||
|
fun healthCheck(@Suppress("UNUSED_PARAMETER") request: Request) =
|
||||||
|
if (healthCheckService.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package be.simplenotes.app.controllers
|
||||||
|
|
||||||
|
import be.simplenotes.app.extensions.html
|
||||||
|
import be.simplenotes.app.extensions.redirect
|
||||||
|
import be.simplenotes.domain.usecases.NoteService
|
||||||
|
import be.simplenotes.domain.usecases.markdown.InvalidMeta
|
||||||
|
import be.simplenotes.domain.usecases.markdown.MissingMeta
|
||||||
|
import be.simplenotes.domain.usecases.markdown.ValidationError
|
||||||
|
import be.simplenotes.types.LoggedInUser
|
||||||
|
import be.simplenotes.views.NoteView
|
||||||
|
import org.http4k.core.Method
|
||||||
|
import org.http4k.core.Request
|
||||||
|
import org.http4k.core.Response
|
||||||
|
import org.http4k.core.Status.Companion.BAD_REQUEST
|
||||||
|
import org.http4k.core.Status.Companion.NOT_FOUND
|
||||||
|
import org.http4k.core.Status.Companion.OK
|
||||||
|
import org.http4k.core.body.form
|
||||||
|
import org.http4k.routing.path
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class NoteController(
|
||||||
|
private val view: NoteView,
|
||||||
|
private val noteService: NoteService,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun new(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
if (request.method == Method.GET) return Response(OK).html(view.noteEditor(loggedInUser))
|
||||||
|
|
||||||
|
val markdownForm = request.form("markdown") ?: ""
|
||||||
|
|
||||||
|
return noteService.create(loggedInUser, markdownForm).fold(
|
||||||
|
{
|
||||||
|
val html = when (it) {
|
||||||
|
MissingMeta -> view.noteEditor(
|
||||||
|
loggedInUser,
|
||||||
|
error = "Missing note metadata",
|
||||||
|
textarea = markdownForm
|
||||||
|
)
|
||||||
|
InvalidMeta -> view.noteEditor(
|
||||||
|
loggedInUser,
|
||||||
|
error = "Invalid note metadata",
|
||||||
|
textarea = markdownForm
|
||||||
|
)
|
||||||
|
is ValidationError -> view.noteEditor(
|
||||||
|
loggedInUser,
|
||||||
|
validationErrors = it.validationErrors,
|
||||||
|
textarea = markdownForm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Response(BAD_REQUEST).html(html)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Response.redirect("/notes/${it.uuid}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun list(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
|
||||||
|
val tag = request.query("tag")
|
||||||
|
val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag)
|
||||||
|
val deletedCount = noteService.countDeleted(loggedInUser.userId)
|
||||||
|
return Response(OK).html(view.notes(loggedInUser, notes, currentPage, pages, deletedCount, tag = tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
val query = request.form("search") ?: ""
|
||||||
|
val notes = noteService.search(loggedInUser.userId, query)
|
||||||
|
val deletedCount = noteService.countDeleted(loggedInUser.userId)
|
||||||
|
return Response(OK).html(view.search(loggedInUser, notes, query, deletedCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun note(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||||
|
|
||||||
|
if (request.method == Method.POST) {
|
||||||
|
if (request.form("delete") != null) {
|
||||||
|
return if (noteService.trash(loggedInUser.userId, noteUuid))
|
||||||
|
Response.redirect("/notes") // TODO: flash cookie to show success ?
|
||||||
|
else
|
||||||
|
Response(NOT_FOUND) // TODO: show an error
|
||||||
|
}
|
||||||
|
if (request.form("public") != null) {
|
||||||
|
if (!noteService.makePublic(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND)
|
||||||
|
} else if (request.form("private") != null) {
|
||||||
|
if (!noteService.makePrivate(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND)
|
||||||
|
return Response(OK).html(view.renderedNote(loggedInUser, note, shared = false))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun public(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||||
|
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||||
|
val note = noteService.findPublic(noteUuid) ?: return Response(NOT_FOUND)
|
||||||
|
return Response(OK).html(view.renderedNote(loggedInUser, note, shared = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun edit(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||||
|
val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND)
|
||||||
|
|
||||||
|
if (request.method == Method.GET) {
|
||||||
|
return Response(OK).html(view.noteEditor(loggedInUser, textarea = note.markdown))
|
||||||
|
}
|
||||||
|
|
||||||
|
val markdownForm = request.form("markdown") ?: ""
|
||||||
|
|
||||||
|
return noteService.update(loggedInUser, note.uuid, markdownForm).fold(
|
||||||
|
{
|
||||||
|
val html = when (it) {
|
||||||
|
MissingMeta -> view.noteEditor(
|
||||||
|
loggedInUser,
|
||||||
|
error = "Missing note metadata",
|
||||||
|
textarea = markdownForm
|
||||||
|
)
|
||||||
|
InvalidMeta -> view.noteEditor(
|
||||||
|
loggedInUser,
|
||||||
|
error = "Invalid note metadata",
|
||||||
|
textarea = markdownForm
|
||||||
|
)
|
||||||
|
is ValidationError -> view.noteEditor(
|
||||||
|
loggedInUser,
|
||||||
|
validationErrors = it.validationErrors,
|
||||||
|
textarea = markdownForm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Response(BAD_REQUEST).html(html)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Response.redirect("/notes/${note.uuid}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trash(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
|
||||||
|
val tag = request.query("tag")
|
||||||
|
val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag, deleted = true)
|
||||||
|
return Response(OK).html(view.trash(loggedInUser, notes, currentPage, pages))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleted(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
val uuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||||
|
return if (request.form("delete") != null)
|
||||||
|
if (noteService.delete(loggedInUser.userId, uuid))
|
||||||
|
Response.redirect("/notes/trash")
|
||||||
|
else
|
||||||
|
Response(NOT_FOUND)
|
||||||
|
else if (noteService.restore(loggedInUser.userId, uuid))
|
||||||
|
Response.redirect("/notes/$uuid")
|
||||||
|
else
|
||||||
|
Response(NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Request.uuidPath(): UUID? {
|
||||||
|
val uuidPath = path("uuid")!!
|
||||||
|
return try {
|
||||||
|
UUID.fromString(uuidPath)!!
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package be.simplenotes.app.controllers
|
||||||
|
|
||||||
|
import be.simplenotes.app.extensions.html
|
||||||
|
import be.simplenotes.app.extensions.redirect
|
||||||
|
import be.simplenotes.domain.usecases.UserService
|
||||||
|
import be.simplenotes.domain.usecases.users.delete.DeleteError
|
||||||
|
import be.simplenotes.domain.usecases.users.delete.DeleteForm
|
||||||
|
import be.simplenotes.types.LoggedInUser
|
||||||
|
import be.simplenotes.views.SettingView
|
||||||
|
import org.http4k.core.*
|
||||||
|
import org.http4k.core.body.form
|
||||||
|
import org.http4k.core.cookie.invalidateCookie
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class SettingsController(
|
||||||
|
private val userService: UserService,
|
||||||
|
private val settingView: SettingView,
|
||||||
|
) {
|
||||||
|
fun settings(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
if (request.method == Method.GET)
|
||||||
|
return Response(Status.OK).html(settingView.settings(loggedInUser))
|
||||||
|
|
||||||
|
val deleteForm = request.deleteForm(loggedInUser)
|
||||||
|
val result = userService.delete(deleteForm)
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
{
|
||||||
|
when (it) {
|
||||||
|
DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Bearer")
|
||||||
|
DeleteError.WrongPassword -> Response(Status.OK).html(
|
||||||
|
settingView.settings(
|
||||||
|
loggedInUser,
|
||||||
|
error = "Wrong password"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
is DeleteError.InvalidForm -> Response(Status.OK).html(
|
||||||
|
settingView.settings(
|
||||||
|
loggedInUser,
|
||||||
|
validationErrors = it.validationErrors
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Response.redirect("/").invalidateCookie("Bearer")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attachment(filename: String, contentType: String) = { response: Response ->
|
||||||
|
val name = filename.replace("[^a-zA-Z0-9-_.]".toRegex(), "_")
|
||||||
|
response
|
||||||
|
.header("Content-Disposition", "attachment; filename=\"$name\"")
|
||||||
|
.header("Content-Type", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun export(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
|
val isDownload = request.form("download") != null
|
||||||
|
|
||||||
|
return if (isDownload) {
|
||||||
|
val filename = "simplenotes-export-${loggedInUser.username}"
|
||||||
|
if (request.form("format") == "zip") {
|
||||||
|
val zip = userService.exportAsZip(loggedInUser.userId)
|
||||||
|
Response(Status.OK)
|
||||||
|
.with(attachment("$filename.zip", "application/zip"))
|
||||||
|
.body(zip)
|
||||||
|
} else
|
||||||
|
Response(Status.OK)
|
||||||
|
.with(attachment("$filename.json", "application/json"))
|
||||||
|
.body(userService.exportAsJson(loggedInUser.userId))
|
||||||
|
} else Response(Status.OK).body(userService.exportAsJson(loggedInUser.userId)).header(
|
||||||
|
"Content-Type",
|
||||||
|
"application/json"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Request.deleteForm(loggedInUser: LoggedInUser) =
|
||||||
|
DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package be.simplenotes.app.controllers
|
||||||
|
|
||||||
|
import be.simplenotes.app.extensions.html
|
||||||
|
import be.simplenotes.app.extensions.isSecure
|
||||||
|
import be.simplenotes.app.extensions.redirect
|
||||||
|
import be.simplenotes.config.JwtConfig
|
||||||
|
import be.simplenotes.domain.usecases.UserService
|
||||||
|
import be.simplenotes.domain.usecases.users.login.*
|
||||||
|
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
|
||||||
|
import be.simplenotes.domain.usecases.users.register.RegisterForm
|
||||||
|
import be.simplenotes.domain.usecases.users.register.UserExists
|
||||||
|
import be.simplenotes.types.LoggedInUser
|
||||||
|
import be.simplenotes.views.UserView
|
||||||
|
import org.http4k.core.Method.GET
|
||||||
|
import org.http4k.core.Request
|
||||||
|
import org.http4k.core.Response
|
||||||
|
import org.http4k.core.Status.Companion.OK
|
||||||
|
import org.http4k.core.body.form
|
||||||
|
import org.http4k.core.cookie.Cookie
|
||||||
|
import org.http4k.core.cookie.SameSite
|
||||||
|
import org.http4k.core.cookie.cookie
|
||||||
|
import org.http4k.core.cookie.invalidateCookie
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class UserController(
|
||||||
|
private val userService: UserService,
|
||||||
|
private val userView: UserView,
|
||||||
|
private val jwtConfig: JwtConfig,
|
||||||
|
) {
|
||||||
|
fun register(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||||
|
if (request.method == GET) return Response(OK).html(
|
||||||
|
userView.register(loggedInUser)
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = userService.register(request.registerForm())
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
{
|
||||||
|
val html = when (it) {
|
||||||
|
UserExists -> userView.register(
|
||||||
|
loggedInUser,
|
||||||
|
error = "User already exists"
|
||||||
|
)
|
||||||
|
is InvalidRegisterForm ->
|
||||||
|
userView.register(
|
||||||
|
loggedInUser,
|
||||||
|
validationErrors = it.validationErrors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Response(OK).html(html)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Response.redirect("/login")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Request.registerForm() = RegisterForm(form("username"), form("password"))
|
||||||
|
private fun Request.loginForm(): LoginForm = registerForm()
|
||||||
|
|
||||||
|
fun login(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||||
|
if (request.method == GET) return Response(OK).html(
|
||||||
|
userView.login(loggedInUser)
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = userService.login(request.loginForm())
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
{
|
||||||
|
val html = when (it) {
|
||||||
|
Unregistered ->
|
||||||
|
userView.login(
|
||||||
|
loggedInUser,
|
||||||
|
error = "User does not exist"
|
||||||
|
)
|
||||||
|
WrongPassword ->
|
||||||
|
userView.login(
|
||||||
|
loggedInUser,
|
||||||
|
error = "Wrong password"
|
||||||
|
)
|
||||||
|
is InvalidLoginForm ->
|
||||||
|
userView.login(
|
||||||
|
loggedInUser,
|
||||||
|
validationErrors = it.validationErrors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Response(OK).html(html)
|
||||||
|
},
|
||||||
|
{ token ->
|
||||||
|
Response.redirect("/notes").loginCookie(token, request.isSecure())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Response.loginCookie(token: Token, secure: Boolean): Response {
|
||||||
|
val validityInSeconds = TimeUnit.SECONDS.convert(jwtConfig.validity, jwtConfig.timeUnit)
|
||||||
|
|
||||||
|
return this.cookie(
|
||||||
|
Cookie(
|
||||||
|
name = "Bearer",
|
||||||
|
value = token,
|
||||||
|
path = "/",
|
||||||
|
httpOnly = true,
|
||||||
|
sameSite = SameSite.Lax,
|
||||||
|
maxAge = validityInSeconds,
|
||||||
|
secure = secure
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logout(@Suppress("UNUSED_PARAMETER") request: Request) = Response.redirect("/")
|
||||||
|
.invalidateCookie("Bearer")
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package be.simplenotes.app.extensions
|
||||||
|
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.http4k.asString
|
||||||
|
import org.http4k.core.Body
|
||||||
|
import org.http4k.core.ContentType
|
||||||
|
import org.http4k.core.Request
|
||||||
|
import org.http4k.core.Response
|
||||||
|
import org.http4k.core.Status.Companion.FOUND
|
||||||
|
import org.http4k.core.Status.Companion.MOVED_PERMANENTLY
|
||||||
|
import org.http4k.lens.*
|
||||||
|
|
||||||
|
fun Response.html(html: String) = body(html)
|
||||||
|
.header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
.header("Cache-Control", "no-cache")
|
||||||
|
|
||||||
|
fun Response.Companion.redirect(url: String, permanent: Boolean = false) =
|
||||||
|
Response(if (permanent) MOVED_PERMANENTLY else FOUND).header("Location", url)
|
||||||
|
|
||||||
|
fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
|
||||||
|
|
||||||
|
val bodyLens = httpBodyRoot(
|
||||||
|
listOf(Meta(true, "body", ParamMeta.ObjectParam, "body")),
|
||||||
|
ContentType.APPLICATION_JSON.withNoDirectives(),
|
||||||
|
ContentNegotiation.StrictNoDirective
|
||||||
|
).map(
|
||||||
|
{ it.payload.asString() },
|
||||||
|
{ Body(it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
inline fun <reified T> Json.auto(): BiDiBodyLensSpec<T> = bodyLens.map(
|
||||||
|
{ decodeFromString(it) },
|
||||||
|
{ encodeToString(it) }
|
||||||
|
)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package be.simplenotes.app.filters
|
||||||
|
|
||||||
|
import be.simplenotes.app.extensions.html
|
||||||
|
import be.simplenotes.views.ErrorView
|
||||||
|
import be.simplenotes.views.ErrorView.Type.*
|
||||||
|
import org.http4k.core.*
|
||||||
|
import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR
|
||||||
|
import org.http4k.core.Status.Companion.NOT_FOUND
|
||||||
|
import org.http4k.core.Status.Companion.NOT_IMPLEMENTED
|
||||||
|
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.sql.SQLTransientException
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ErrorFilter(private val errorView: ErrorView) : Filter {
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
private fun errorResponse(status: Status): Response {
|
||||||
|
val type = when (status) {
|
||||||
|
SERVICE_UNAVAILABLE -> SqlTransientError
|
||||||
|
NOT_FOUND -> NotFound
|
||||||
|
NOT_IMPLEMENTED -> Other
|
||||||
|
else -> Other
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(status).html(errorView.error(type)).noCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invoke(next: HttpHandler): HttpHandler = { request ->
|
||||||
|
try {
|
||||||
|
val response = next(request)
|
||||||
|
if (response.status == NOT_FOUND) errorResponse(NOT_FOUND)
|
||||||
|
else response
|
||||||
|
} catch (e: SQLTransientException) {
|
||||||
|
logger.error(e.stackTraceToString())
|
||||||
|
errorResponse(SERVICE_UNAVAILABLE)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e.stackTraceToString())
|
||||||
|
errorResponse(INTERNAL_SERVER_ERROR)
|
||||||
|
} catch (e: NotImplementedError) {
|
||||||
|
logger.error(e.stackTraceToString())
|
||||||
|
errorResponse(NOT_IMPLEMENTED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package be.simplenotes.app.filters
|
||||||
|
|
||||||
|
import org.http4k.core.Filter
|
||||||
|
import org.http4k.core.HttpHandler
|
||||||
|
import org.http4k.core.Request
|
||||||
|
import org.http4k.core.Status.Companion.OK
|
||||||
|
|
||||||
|
object ImmutableFilter : Filter {
|
||||||
|
override fun invoke(next: HttpHandler) = { request: Request ->
|
||||||
|
val res = next(request)
|
||||||
|
if (res.status == OK)
|
||||||
|
res.header("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
else res
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package be.simplenotes.app.filters
|
||||||
|
|
||||||
|
import org.http4k.core.Filter
|
||||||
|
import org.http4k.core.HttpHandler
|
||||||
|
import org.http4k.core.Request
|
||||||
|
|
||||||
|
object SecurityFilter : Filter {
|
||||||
|
override fun invoke(next: HttpHandler): HttpHandler = { request: Request ->
|
||||||
|
val response = next(request)
|
||||||
|
.header("X-Content-Type-Options", "nosniff")
|
||||||
|
|
||||||
|
if (response.header("Content-Type")?.contains("text/html") == true) {
|
||||||
|
response
|
||||||
|
.header("Content-Security-Policy", "default-src 'self'")
|
||||||
|
.header("Referrer-Policy", "no-referrer")
|
||||||
|
} else response
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package be.simplenotes.app.filters.auth
|
||||||
|
|
||||||
|
import be.simplenotes.types.LoggedInUser
|
||||||
|
import org.http4k.core.Request
|
||||||
|
import org.http4k.core.cookie.cookie
|
||||||
|
import org.http4k.lens.BiDiLens
|
||||||
|
|
||||||
|
typealias OptionalAuthLens = BiDiLens<@JvmSuppressWildcards Request, @JvmSuppressWildcards LoggedInUser?>
|
||||||
|
typealias RequiredAuthLens = BiDiLens<@JvmSuppressWildcards Request, @JvmSuppressWildcards LoggedInUser>
|
||||||
|
|
||||||
|
enum class JwtSource {
|
||||||
|
Header, Cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Request.bearerTokenCookie(): String? = cookie("Bearer")
|
||||||
|
?.value
|
||||||
|
?.trim()
|
||||||
|
|
||||||
|
fun Request.bearerTokenHeader(): String? =
|
||||||
|
header("Authorization")
|
||||||
|
?.trim()
|
||||||
|
?.takeIf { it.startsWith("Bearer") }
|
||||||
|
?.substringAfter("Bearer")
|
||||||
|
?.trim()
|
||||||