155 Commits

Author SHA1 Message Date
hubert 235e8b6e3c Use gradle catalogs 2021-05-06 12:22:16 +02:00
hubert 1e0fe12396 Upgrade kotlin, kotlinx-html, gradle, shadow 2021-05-06 12:22:16 +02:00
hubert 7ad8b7039b Update config 2021-04-12 21:15:42 +02:00
hubert 204ae7988e Update ktorm, clean repositories and drop mariadb support 2021-04-12 18:35:21 +02:00
hubert a4bf998c5b Remove Boilerplate Use-case thingy 2021-03-03 16:57:53 +01:00
hubert 3e1683dfe5 Use a proper search input parser 2021-03-03 14:28:34 +01:00
hubert 51b682c593 Skip slow tests by default 2021-03-03 14:28:34 +01:00
hubert ea110d51d3 Simplify configuration 2021-02-27 20:34:44 +01:00
hubert f255064533 Upgrade versions, etc 2021-02-27 18:44:19 +01:00
hubert 761382da23 Fix typos 2021-02-27 18:07:22 +01:00
hubert 525e3a4a3f Update kotlin + gradle + dependencies 2021-02-06 01:05:33 +01:00
hubert 69e50b158f Update kotlin -> 1.4.20 & java -> 15 2020-11-29 22:22:09 +01:00
hubert 8997433974 Update dependencies 2020-11-29 22:20:40 +01:00
hubert 909fb482a8 Clean gradle build 2020-11-29 21:15:31 +01:00
hubert 90701dcdce Refactor jwt 2020-11-11 23:48:27 +01:00
hubert 8439782430 Flatten packages
Remove modules prefix
2020-11-11 23:48:27 +01:00
hubert e6a7af840a Fix db health check 2020-11-06 23:44:57 +01:00
hubert 568a2c6831 Move search parsing to domain layer 2020-11-06 23:44:37 +01:00
hubert c39a20cf96 Move health check to domain layer 2020-11-05 14:44:59 +01:00
hubert bf56314473 Move transactions to domain layer 2020-11-05 14:37:20 +01:00
hubert 11caff1634 Format 2020-11-03 18:36:09 +01:00
hubert b1478fd154 Why ?! 2020-11-03 18:36:09 +01:00
hubert dd174a6327 Log discarded html tags + small refactor 2020-11-03 18:36:09 +01:00
hubert 941380ad16 Load configuration with micronaut 2020-11-03 18:36:09 +01:00
hubert 4f395d254d Merge branch 'micronaut' into master 2020-11-02 00:34:31 +01:00
hubert 6a43acfd46 Switch from koin to micronaut-inject 2020-11-02 00:33:57 +01:00
hubert 78b84dc62a Add more persistance tests 2020-11-02 00:33:57 +01:00
hubert 1120bc9350 Add .sdkmanrc 2020-10-29 16:57:39 +01:00
hubert a37254452b Fix webmanifest 2020-10-28 02:58:30 +01:00
hubert cd9fdd28e8 Gradle stuff 2020-10-28 02:01:16 +01:00
hubert c3fc6a4e88 Clean gradle scripts 2020-10-28 00:42:51 +01:00
hubert cb58a4fbe0 Fix lucene illegal reflective access warning 2020-10-26 22:35:27 +01:00
hubert 64059984d3 Nice 2020-10-26 22:24:56 +01:00
hubert fdc8d34f82 Move postcss purge config to gradle 2020-10-26 22:19:15 +01:00
hubert 95ec674eb8 Fix ignored gradle wrapper jar 2020-10-26 21:18:02 +01:00
hubert ea7be84ec3 Extract serialization plugin 2020-10-26 21:17:19 +01:00
hubert c709f2b44d Add ktlint plugin 2020-10-26 21:17:19 +01:00
hubert 7995a0b3e0 Change arrow core -> arrow core data 2020-10-26 21:15:59 +01:00
hubert bfd562bc60 Add Gradle Wrapper 2020-10-26 02:10:22 +01:00
hubert 4fb85a52e4 Use Gradle ! 2020-10-26 02:10:22 +01:00
hubert e64352f54c Fix arrow depreciations 2020-10-25 23:46:56 +01:00
hubert c2c03e415e Move Logback.xml 2020-10-24 01:36:51 +02:00
hubert 0260bea951 Move ConfigLoader 2020-10-24 01:36:51 +02:00
hubert 8b8dbd6fe5 Separate views into a maven module 2020-10-24 01:36:51 +02:00
hubert 536c6e7b79 Merge branch 'jigsaw' into master 2020-10-24 00:05:09 +02:00
hubert e5a2b8993f Remove module-infos
Some libraires are not yet ready..

{Arrow,Lucene,Http4k} should be packed inside a shaded jar because they
all have some split packages

HikariCP has an invalid module-info.java

Kapt doesn't work
2020-10-23 23:20:58 +02:00
hubert 38750a588c Move packages + remove circular dependencies 2020-10-23 23:20:58 +02:00
hubert ee026ec829 Move junit config to simplenotes-test-resources 2020-10-23 16:36:44 +02:00
hubert 29b024d360 Move ArrowAssertions to simplenotes-domain 2020-10-23 16:30:43 +02:00
hubert cd12d1561a Move config into simplenotes-config module 2020-10-23 16:24:50 +02:00
hubert c2eaf3d0cc Move types into simplenotes-types module 2020-10-23 16:12:40 +02:00
hubert 4c9ac8944e Prefix maven modules 2020-10-23 15:45:28 +02:00
hubert 4ff97044f0 Change public/private button css 2020-10-23 08:26:03 +02:00
hubert ead1932d48 Fix some css 2020-10-23 07:16:07 +02:00
hubert 4a7dcec363 Use Json lenses 2020-10-23 06:22:44 +02:00
hubert cb76a3253d Use mapstruct 2020-10-21 22:55:36 +02:00
hubert 681fd635b3 Add health check 2020-10-21 16:30:42 +02:00
hubert 9467db2382 Use transactions at the http layer 2020-10-20 23:26:20 +02:00
hubert 7ed3494808 Clean pom common dependencies 2020-10-20 19:21:29 +02:00
hubert ceb310bf02 Fix favicon extension 2020-10-20 16:10:13 +02:00
hubert 2c3106c5c1 Clean homepage html 2020-10-20 15:56:24 +02:00
hubert 4effa8231a Change icons 2020-10-20 15:55:11 +02:00
hubert b78420e106 Update some dependencies 2020-10-17 03:39:44 +02:00
hubert dd08763161 We don't need that much RAM 2020-09-30 18:57:46 +02:00
hubert e0b1514965 Unload bootstraping modules after startup 2020-09-30 18:56:52 +02:00
hubert 69c91ec86a Proper shutdown 2020-09-30 00:18:09 +02:00
hubert 1bc45461c3 Small refactor 2020-09-30 00:10:37 +02:00
hubert 0dfb2a7e03 Add zip export 2020-09-28 19:21:53 +02:00
hubert a7c8e63b11 Add radio input in order to select the export format 2020-09-28 18:29:07 +02:00
hubert ad97ba029e Fix small bug 2020-09-02 22:13:08 +02:00
hubert 31f538c7f5 Add python cli 2020-09-02 21:45:17 +02:00
hubert c7cf71441f Add API 2020-09-02 21:41:57 +02:00
hubert b015f3a97e Add instructions 2020-08-25 08:18:34 +02:00
hubert b8e9d4e96e Fix a test 2020-08-25 07:12:58 +02:00
hubert c5f9a1d6e0 Add possibility to share notes 2020-08-25 07:04:29 +02:00
hubert 1cb6c731d8 Css 2020-08-24 19:16:21 +02:00
hubert f6e33ed3b4 Markdown improvements 2020-08-24 18:50:54 +02:00
hubert 815770303c Add caddy config 2020-08-24 00:53:58 +02:00
hubert d6ab257473 Set nocache for templates 2020-08-24 00:53:00 +02:00
hubert cbc3a017e8 More UI improvements 2020-08-23 01:10:06 +02:00
hubert d70663b898 UI improvements 2020-08-23 00:38:20 +02:00
hubert 4ffa565626 Fix cookie invalidation when deleting account 2020-08-22 23:32:19 +02:00
hubert ea732325e5 Css + fonts.. 2020-08-22 23:17:57 +02:00
hubert f73a9d0b96 Html + css improvements 2020-08-22 18:57:33 +02:00
hubert 5573dd45d6 Reduce cookie size 2020-08-22 17:46:32 +02:00
hubert eeae982a71 Fix linting warnings 2020-08-22 05:12:09 +02:00
hubert bad5322abd Seo 2020-08-22 05:12:09 +02:00
hubert 39fe7a5ab6 Add LICENSE 2020-08-22 03:00:13 +00:00
hubert c800d22ccf Accessibility stuff 2020-08-22 04:29:41 +02:00
hubert 2ac02688ab More buttons 2020-08-22 03:23:24 +02:00
hubert 7d833e48e1 css.. 2020-08-22 03:10:04 +02:00
hubert 97ea331c51 Landing page + css.. 2020-08-22 02:46:54 +02:00
hubert 4c4ca2dd98 Navbar css 2020-08-22 00:14:10 +02:00
hubert 204f2fb002 Better code css 2020-08-21 23:40:10 +02:00
hubert 87a6fdc94a Change code bg color 2020-08-21 22:47:25 +02:00
hubert fa21c85a13 Add highlight js 2020-08-21 22:20:36 +02:00
hubert c367d5b613 Add time ago 2020-08-21 20:59:49 +02:00
hubert 2c967ebd8c Fix a bug where no tags would return an empty tag while searching 2020-08-21 20:38:59 +02:00
hubert 381d935875 add clear search button 2020-08-21 20:21:47 +02:00
hubert d575773f73 Update dependencies + pin mariadb version 2020-08-21 19:55:02 +02:00
hubert 36600bb1f4 Better error handling 2020-08-21 19:31:24 +02:00
hubert b27fd29230 Index md instead of html 2020-08-21 18:03:33 +02:00
hubert c02f7c039a Keep search query on reload 2020-08-21 17:44:43 +02:00
hubert 8ba89d3e05 Search now apply to all fields by default 2020-08-21 17:04:14 +02:00
hubert 372652d332 Improve dsl 2020-08-21 16:28:03 +02:00
hubert f12947acbd Create lucene dsl 2020-08-21 16:02:10 +02:00
hubert 7305fb47c7 Merge branch 'lucene-search' 2020-08-19 19:32:37 +02:00
hubert a440199006 Fix unsupported method on jdk backend 2020-08-19 19:31:56 +02:00
hubert 1432fbb395 Update docker + pom 2020-08-19 19:17:06 +02:00
hubert 08c804ccb5 Fix circular dependency 2020-08-19 19:02:15 +02:00
hubert 68109f8666 Drop indexes + view 2020-08-19 18:47:19 +02:00
hubert 315a01ea18 Add search terms parser + tests 2020-08-19 18:21:52 +02:00
hubert 12619f6550 Index all notes at start 2020-08-19 16:45:55 +02:00
hubert ab3766b8b8 Add searcher to note service 2020-08-19 05:20:17 +02:00
hubert 3861fb6b97 Add search module 2020-08-19 05:08:27 +02:00
hubert 88b6eb56ae Merge branch 'export' 2020-08-19 02:37:29 +02:00
hubert 8ccd7f6058 Show deleted notes count 2020-08-19 00:16:00 +02:00
hubert a98d6e8e64 Change docker tag 2020-08-18 19:52:04 +02:00
hubert ab7cfd8147 Merge branch 'trash' 2020-08-18 19:51:20 +02:00
hubert 5d9ca85b22 Trash feature done 2020-08-18 19:51:13 +02:00
hubert 15de81394c Persistance: Note can now be put in the trash 2020-08-17 18:51:33 +02:00
hubert 4e2fe463e0 Merge branch 'list-by-tags' 2020-08-17 17:21:52 +02:00
hubert 0e72547e95 tag class style improvements 2020-08-17 17:21:11 +02:00
hubert 845ca2acb8 Click on tag from a single note 2020-08-17 17:18:05 +02:00
hubert 25e29afcbb Validate tags with regex 2020-08-17 17:15:14 +02:00
hubert 5295e32d86 Find notes by tags in notes list 2020-08-17 17:09:48 +02:00
hubert 8021814c31 Find notes by tags 2020-08-17 16:44:16 +02:00
hubert b5beca8661 Update kotlin version 2020-08-17 16:01:29 +02:00
hubert 56e742e39f Improve sql 2020-08-17 16:01:15 +02:00
hubert 48897c8b90 Remove docker compose port binding 2020-08-16 19:24:03 +02:00
hubert 9d9ec013f5 Keep details open in /settings if refresh 2020-08-14 23:03:33 +02:00
hubert 01ba7cbd7d Fix redundant padding 2020-08-14 22:29:39 +02:00
hubert 4984e488ae Add /settings link 2020-08-14 21:24:50 +02:00
hubert 662d6c706b Accounts can now be deleted 2020-08-14 21:15:28 +02:00
hubert 00dafe1da9 Oops 2020-08-14 17:26:19 +02:00
hubert a9bbfcf82c Moved css files 2020-08-14 17:23:38 +02:00
hubert 4c38512038 Split postcss files 2020-08-14 17:19:23 +02:00
hubert fb49d45677 More buttons 2020-08-14 16:12:17 +02:00
hubert 90f6709885 Use btn css class 2020-08-14 15:56:41 +02:00
hubert b90df61020 Lint 2020-08-14 15:35:45 +02:00
hubert 1b79635ffa Persists login cookie between browser restarts 2020-08-14 15:29:47 +02:00
hubert 934820274b Leaner docker image 2020-08-14 14:56:34 +02:00
hubert 24ac5cf4fc Add tasklist markdown extension 2020-08-14 02:32:05 +02:00
hubert 29e445ff41 Add Referrer-Policy 2020-08-13 23:58:54 +02:00
hubert e65a4e10d6 Fix mariadb utf8 encoding.. 2020-08-13 20:39:29 +02:00
hubert 24aabd494e Merge http4k 2020-08-13 19:39:41 +02:00
hubert b41b2103f0 That too 2020-07-19 01:54:20 +02:00
hubert a11450cbcf Simplify form handling 2020-07-19 01:53:18 +02:00
hubert d419b4c72a Fix a bug for secure cookies 2020-07-18 20:52:33 +02:00
hubert 9216696b1a Secure cookies 2020-07-18 20:41:42 +02:00
hubert cdfe1d14ef Rename templates to get syntax highlighting 2020-07-18 17:21:52 +02:00
hubert fda355a690 Handle 404 for notes 2020-07-18 17:05:03 +02:00
hubert fc883373d0 Fix password input type 2020-07-18 15:54:22 +02:00
hubert 50020c2f91 Small fixes 2020-07-18 15:42:54 +02:00
hubert 44b463d9d5 Remove nuxt + 100 other things.. 2020-07-18 15:02:09 +02:00
275 changed files with 8763 additions and 13648 deletions
+1 -2
View File
@@ -13,5 +13,4 @@ insert_final_newline = true
indent_size = 4
insert_final_newline = true
max_line_length = 120
disabled_rules = no-wildcard-imports
kotlin_imports_layout = idea
disabled_rules = no-wildcard-imports,import-ordering
-8
View File
@@ -1,9 +1 @@
MYSQL_ROOT_PASSWORD=
MYSQL_HOST=db
MYSQL_DATABASE=
MYSQL_USER=
MYSQL_PASSWORD=
JWT_SECRET=
JWT_REFRESH_SECRET=
CORS=false
PORT=8081
+10 -89
View File
@@ -1,24 +1,6 @@
# Java
.mtj.tmp/
*.class
*.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
# Gradle
build/
.gradle
# IntelliJ
out/
@@ -28,11 +10,8 @@ out/
*.ipr
*.iws
# Vue
node_modules
/dist
# Local env files
.env
.env.local
.env.*.local
@@ -49,77 +28,19 @@ pids
*.seed
*.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
node_modules/
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
# dotenv environment variables file
.env
# h2 db
*.db
# parcel-bundler cache (https://parceljs.org/)
.cache
# lucene index
.lucene/
# next.js build output
.next
# python
__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/
+5
View File
@@ -0,0 +1,5 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=15-open
gradle=6.8-rc-1
kotlin=1.4.20
-104
View File
@@ -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;"
}
+39
View File
@@ -0,0 +1,39 @@
FROM openjdk:15-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
RUN mkdir /app
RUN mkdir /app/data
COPY --from=jdkbuilder /myjdk /myjdk
COPY app/build/libs/app-with-dependencies*.jar /app/simplenotes.jar
WORKDIR /app
VOLUME /app/data
ENV SERVER_HOST 0.0.0.0
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" \
]
-28
View File
@@ -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"]
+21
View File
@@ -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.
+18
View File
@@ -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.
-8
View File
@@ -1,8 +0,0 @@
FORMAT: 1A
HOST: http://localhost:5000
# Notes API
<!-- include(./users/index.apib) -->
<!-- include(./notes/index.apib) -->
<!-- include(./tags/index.apib) -->
-163
View File
@@ -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
-14
View File
@@ -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])
-86
View File
@@ -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)
-110
View File
@@ -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
View File
@@ -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
Vendored
-286
View File
@@ -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 "$@"
-161
View File
@@ -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%
-317
View File
@@ -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>
-21
View File
@@ -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
-21
View File
@@ -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)
-56
View File
@@ -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() }
}
-54
View File
@@ -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)
}
}
-17
View File
@@ -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())
}
}
}
})
-20
View File
@@ -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)
}
-9
View File
@@ -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
-21
View File
@@ -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>
}
-11
View File
@@ -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
}
-17
View File
@@ -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
-9
View File
@@ -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()
}
-8
View File
@@ -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)
-36
View File
@@ -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")
-20
View File
@@ -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)
}
-6
View File
@@ -1,6 +0,0 @@
package be.vandewalleh.factories
import me.liuwj.ktorm.database.*
import javax.sql.DataSource
fun databaseFactory(dataSource: DataSource) = Database.connect(dataSource)
-6
View File
@@ -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()
}
}
})
-17
View File
@@ -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)
}
}
})
-31
View File
@@ -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)
-66
View File
@@ -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"
}
-27
View File
@@ -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()
}
}
-13
View File
@@ -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()
}
}
})
-95
View File
@@ -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")
}
}
-17
View File
@@ -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()))
}
}
})
-123
View File
@@ -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)
-144
View File
@@ -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>
}
}
-65
View File
@@ -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("??")
}
}
}
-18
View File
@@ -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
}
-16
View File
@@ -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
}
-14
View File
@@ -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 }
}
-18
View File
@@ -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()
-15
View File
@@ -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"
}
}
-19
View File
@@ -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)
-23
View File
@@ -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)
}
-17
View File
@@ -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)
}
}
-51
View File
@@ -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
-5
View File
@@ -1,5 +0,0 @@
package utils
import org.json.JSONObject
fun JSONObject.keyList(): List<Any?> = keys().asSequence().toList()
-6
View File
@@ -1,6 +0,0 @@
package utils
import am.ik.yavi.core.ConstraintViolations
val ConstraintViolations.firstInvalid: Any?
get() = this.violations().firstOrNull()?.name()
+32
View File
@@ -0,0 +1,32 @@
import be.simplenotes.micronaut
plugins {
id("be.simplenotes.base")
id("be.simplenotes.kotlinx-serialization")
id("be.simplenotes.app-shadow")
id("be.simplenotes.docker")
id("be.simplenotes.micronaut")
}
dependencies {
implementation(project(":domain"))
implementation(project(":views"))
implementation(project(":css"))
implementation(libs.http4k.core)
implementation(libs.bundles.jetty)
implementation(libs.kotlinx.serialization.json)
implementation(libs.slf4j.api)
runtimeOnly(libs.slf4j.logback)
micronaut()
testImplementation(libs.bundles.test)
testImplementation(libs.http4k.testing.hamkrest)
}
docker {
image = "hubv/simplenotes"
tag = "latest"
}
@@ -1,20 +1,17 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<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>
</encoder>
</appender>
<root level="INFO">
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
</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="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.testcontainers" level="INFO"/>
<logger name="com.github.dockerjava" level="WARN"/>
<logger name="🐳 [mariadb:10.3.6]" level="WARN"/>
<logger name="io.micronaut" level="INFO"/>
<logger name="io.micronaut.context.lifecycle" level="INFO"/>
</configuration>
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

+9
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('#note pre code').forEach((b) => {
hljs.highlightBlock(b);
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.
+3
View File
@@ -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

+19
View File
@@ -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"
}
+29
View File
@@ -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()
}
}
+14
View File
@@ -0,0 +1,14 @@
package be.simplenotes.app
import io.micronaut.context.ApplicationContext
import java.lang.Runtime.getRuntime
fun main() {
val env = if (System.getenv("ENV") == "dev") "dev" else "prod"
val ctx = ApplicationContext.builder()
.deduceEnvironment(false)
.environments(env)
.start()
ctx.createBean(Server::class.java)
getRuntime().addShutdownHook(Thread { ctx.stop() })
}
+79
View File
@@ -0,0 +1,79 @@
package be.simplenotes.app.api
import be.simplenotes.app.extensions.auto
import be.simplenotes.domain.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(val query: String)
+31
View File
@@ -0,0 +1,31 @@
package be.simplenotes.app.api
import be.simplenotes.app.extensions.auto
import be.simplenotes.domain.LoginForm
import be.simplenotes.domain.UserService
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)
+15
View File
@@ -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))
}
+166
View File
@@ -0,0 +1,166 @@
package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.redirect
import be.simplenotes.domain.MarkdownParsingError
import be.simplenotes.domain.NoteService
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) {
MarkdownParsingError.MissingMeta -> view.noteEditor(
loggedInUser,
error = "Missing note metadata",
textarea = markdownForm
)
MarkdownParsingError.InvalidMeta -> view.noteEditor(
loggedInUser,
error = "Invalid note metadata",
textarea = markdownForm
)
is MarkdownParsingError.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) {
MarkdownParsingError.MissingMeta -> view.noteEditor(
loggedInUser,
error = "Missing note metadata",
textarea = markdownForm
)
MarkdownParsingError.InvalidMeta -> view.noteEditor(
loggedInUser,
error = "Invalid note metadata",
textarea = markdownForm
)
is MarkdownParsingError.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
}
}
}
+82
View File
@@ -0,0 +1,82 @@
package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.redirect
import be.simplenotes.domain.DeleteError
import be.simplenotes.domain.DeleteForm
import be.simplenotes.domain.ExportService
import be.simplenotes.domain.UserService
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 exportService: ExportService,
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 = exportService.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(exportService.exportAsJson(loggedInUser.userId))
} else Response(Status.OK).body(exportService.exportAsJson(loggedInUser.userId)).header(
"Content-Type",
"application/json"
)
}
private fun Request.deleteForm(loggedInUser: LoggedInUser) =
DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
}
+111
View File
@@ -0,0 +1,111 @@
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.*
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) {
RegisterError.UserExists -> userView.register(
loggedInUser,
error = "User already exists"
)
is RegisterError.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) {
LoginError.Unregistered ->
userView.login(
loggedInUser,
error = "User does not exist"
)
LoginError.WrongPassword ->
userView.login(
loggedInUser,
error = "Wrong password"
)
is LoginError.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")
}
+36
View File
@@ -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) }
)
+47
View File
@@ -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)
}
}
}
+15
View File
@@ -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
}
}
+18
View File
@@ -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
}
}
+24
View File
@@ -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()
@@ -0,0 +1,23 @@
package be.simplenotes.app.filters.auth
import be.simplenotes.app.filters.auth.JwtSource.Cookie
import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.types.LoggedInUser
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.with
class OptionalAuthFilter(
private val simpleJwt: SimpleJwt<LoggedInUser>,
private val lens: OptionalAuthLens,
private val source: JwtSource = Cookie,
) : Filter {
override fun invoke(next: HttpHandler): HttpHandler = {
val token = when (source) {
JwtSource.Header -> it.bearerTokenHeader()
Cookie -> it.bearerTokenCookie()
}
next(it.with(lens of token?.let { simpleJwt.extract(it) }))
}
}

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