Remove nuxt + 100 other things..
This commit is contained in:
parent
3b80ae051d
commit
44b463d9d5
6
.gitignore
vendored
6
.gitignore
vendored
@ -123,3 +123,9 @@ sw.*
|
||||
# Certificates
|
||||
data/
|
||||
letsencrypt/
|
||||
|
||||
# resources
|
||||
|
||||
resources/css-manifest.json
|
||||
resources/docs/index.html
|
||||
resources/static/*.css
|
||||
|
||||
99
Caddyfile
99
Caddyfile
@ -1,104 +1,7 @@
|
||||
(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
|
||||
}
|
||||
|
||||
reverse_proxy http://localhost:8081
|
||||
}
|
||||
|
||||
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;"
|
||||
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
FORMAT: 1A
|
||||
HOST: http://localhost:5000
|
||||
HOST: https://simplenotes.be/api
|
||||
|
||||
# Notes API
|
||||
|
||||
<!-- include(./users/index.apib) -->
|
||||
<!-- include(./notes/index.apib) -->
|
||||
<!-- include(./tags/index.apib) -->
|
||||
<!-- include(./tags/index.apib) -->
|
||||
|
||||
@ -1,162 +1,129 @@
|
||||
# Data Structures
|
||||
|
||||
## Chapter (object)
|
||||
+ title: Chapter 1 (string)
|
||||
+ content: ... (string)
|
||||
|
||||
|
||||
# Group Notes
|
||||
|
||||
## Notes [/notes]
|
||||
|
||||
### Create a Note [POST]
|
||||
|
||||
+ Request (application/json)
|
||||
+ Request (text/markdown; charset=UTF-8)
|
||||
+ 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)
|
||||
|
||||
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
|
||||
+ Body
|
||||
|
||||
---
|
||||
title: example
|
||||
tags: ["some", "tags"]
|
||||
---
|
||||
# A story
|
||||
|
||||
- a
|
||||
- b
|
||||
|
||||
|
||||
+ Response 201 (application/json)
|
||||
+ Attributes (object)
|
||||
+ uuid: `107c90ae-a41e-4c8e-b5e3-1a269cfe044b` (string)
|
||||
|
||||
### Get Notes [GET]
|
||||
{
|
||||
"title": "example",
|
||||
"tags": [
|
||||
"some",
|
||||
"tags"
|
||||
],
|
||||
"markdown": "---\ntitle: example\ntags: [\"some\", \"tags\"]\n---\n# A story\n\n- a\n- b",
|
||||
"html": "<h1>A story</h1>\n<ul><li>a</li><li>b</li></ul>\n",
|
||||
"uuid": "42aa1078-130e-47ee-b82d-b1d62f3ea054",
|
||||
"updatedAt": "2020-07-16T01:03:46.7766"
|
||||
}
|
||||
|
||||
+ 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}]
|
||||
## Notes [/notes{?limit,after}]
|
||||
|
||||
+ Parameters
|
||||
+ noteUuid: `123e4567-e89b-12d3-a456-426614174000` (string) - The note UUID.
|
||||
+ limit: 10 (number, optional) - The number of notes to return
|
||||
+ Default: `20`
|
||||
+ after: `9bd36653-6397-4c5b-b8b7-158d9de208ef` (string, optional) - The UUID of the note before the requested ones
|
||||
|
||||
|
||||
|
||||
### Get All Notes [GET]
|
||||
|
||||
+ Request
|
||||
|
||||
+ Headers
|
||||
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
|
||||
+ Response 200 (application/json)
|
||||
+ Body
|
||||
|
||||
[
|
||||
{
|
||||
"uuid": "42aa1078-130e-47ee-b82d-b1d62f3ea054",
|
||||
"title": "example",
|
||||
"updatedAt": "2020-07-16T01:03:46",
|
||||
"tags": [
|
||||
"some",
|
||||
"tags"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uuid": "e61271e9-ba86-4428-a788-49946d1954c5",
|
||||
"title": "test",
|
||||
"updatedAt": "2020-07-16T00:22:02",
|
||||
"tags": [
|
||||
"babar",
|
||||
"fait",
|
||||
"du",
|
||||
"ski"
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
## Note [/notes/{uuid}]
|
||||
|
||||
+ Parameters
|
||||
+ uuid: `123e4567-e89b-12d3-a456-426614174000` (required, string) - The note UUID.
|
||||
|
||||
|
||||
### Get a Note [GET]
|
||||
|
||||
+ Request (application/json)
|
||||
+ Request
|
||||
+ Headers
|
||||
Authorization: Bearer <token>
|
||||
|
||||
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
|
||||
+ 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
|
||||
+ Body
|
||||
|
||||
|
||||
### 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
|
||||
{
|
||||
"uuid": "8ba68c64-11f1-4424-a0cb-cba54a65298f",
|
||||
"title": "example",
|
||||
"markdown": "---\ntitle: example\ntags: [\"some\", \"tags\"]\n---\n# A story\n\n- a\n- b",
|
||||
"html": "<h1>A story</h1>\n<ul><li>a</li><li>b</li></ul>\n",
|
||||
"updatedAt": "2020-07-16T01:13:37",
|
||||
"tags": [
|
||||
"some",
|
||||
"tags"
|
||||
]
|
||||
}
|
||||
|
||||
+ Response 404
|
||||
|
||||
|
||||
### Update a Note [PUT]
|
||||
|
||||
#### TODO
|
||||
|
||||
### 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)
|
||||
+ Request
|
||||
+ 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>
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
|
||||
+ Response 200
|
||||
|
||||
|
||||
@ -4,11 +4,18 @@
|
||||
|
||||
### Get all tags [GET]
|
||||
|
||||
+ Request (application/json)
|
||||
+ Request
|
||||
+ Headers
|
||||
Authorization: Bearer <token>
|
||||
|
||||
|
||||
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
|
||||
+ Response 200 (application/json)
|
||||
+ Attributes
|
||||
+ tags: Dev, Server (array[string])
|
||||
+ Body
|
||||
|
||||
[
|
||||
"markdown",
|
||||
"md",
|
||||
"code",
|
||||
"java"
|
||||
]
|
||||
|
||||
@ -1,41 +1,37 @@
|
||||
# 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]
|
||||
### Create an account [POST]
|
||||
|
||||
+ Request (application/json)
|
||||
+ Attributes (object)
|
||||
+ username: babar (string)
|
||||
+ password: tortue (string)
|
||||
|
||||
+ Response 200 (application/json)
|
||||
+ Attributes (object)
|
||||
+ message: Created (string)
|
||||
+ Headers
|
||||
|
||||
Accept: application/json
|
||||
|
||||
+ Body
|
||||
|
||||
{
|
||||
"username": "user",
|
||||
"password": "apassword"
|
||||
}
|
||||
|
||||
|
||||
+ Response 200
|
||||
|
||||
+ Response 409
|
||||
|
||||
+ Response 409 (application/json)
|
||||
+ Attributes (object)
|
||||
+ message: User already exists (string)
|
||||
|
||||
### Delete a user [DELETE]
|
||||
|
||||
+ Request
|
||||
+ Headers
|
||||
Authorization: Bearer <token>
|
||||
|
||||
+ Response 200 (application/json)
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
|
||||
+ Response 200
|
||||
|
||||
+ Response 404
|
||||
|
||||
|
||||
## Authentication [/user/login]
|
||||
@ -44,43 +40,72 @@ Authenticate one user to access protected routing.
|
||||
### Authenticate a user [POST]
|
||||
|
||||
+ Request (application/json)
|
||||
+ Attributes (Login)
|
||||
+ Headers
|
||||
|
||||
Accept: application/json
|
||||
|
||||
+ Body
|
||||
|
||||
{
|
||||
"username": "user",
|
||||
"password": "myrealpassword"
|
||||
}
|
||||
|
||||
+ Response 200 (application/json)
|
||||
+ Attributes
|
||||
+ token: <token>
|
||||
+ refreshToken: `<refresh-token>`
|
||||
+ Body
|
||||
|
||||
+ Response 401 (application/json)
|
||||
+ Attributes (InvalidCredentials)
|
||||
{
|
||||
"token": "<token>",
|
||||
"refreshToken": "<token>"
|
||||
}
|
||||
|
||||
+ Response 401
|
||||
|
||||
## Token refresh [/user/refresh_token]
|
||||
|
||||
### Refresh JWT token [POST]
|
||||
|
||||
+ Request (application/json)
|
||||
+ Attributes
|
||||
+ refreshToken: `<refresh-token>`
|
||||
+ Headers
|
||||
|
||||
Accept: application/json
|
||||
|
||||
+ Body
|
||||
|
||||
{
|
||||
"refreshToken": "<refresh-token>"
|
||||
}
|
||||
|
||||
+ Response 200 (application/json)
|
||||
+ Attributes
|
||||
+ token: <token>
|
||||
+ refreshToken: `<refresh-token>`
|
||||
+ Body
|
||||
|
||||
{
|
||||
"token": "<token>",
|
||||
"refreshToken": "<refresh-token>"
|
||||
}
|
||||
|
||||
|
||||
+ Response 401
|
||||
|
||||
+ 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)
|
||||
+ Request
|
||||
+ Headers
|
||||
Authorization: Bearer <token>
|
||||
|
||||
|
||||
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
|
||||
|
||||
+ Response 200 (application/json)
|
||||
+ Attributes
|
||||
+ user: (object)
|
||||
+ username: babar (string)
|
||||
+ Body
|
||||
|
||||
{
|
||||
"user": {
|
||||
"username": "user"
|
||||
}
|
||||
}
|
||||
|
||||
+ Response 401
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
package be.vandewalleh
|
||||
|
||||
import be.vandewalleh.auth.AuthenticationModule
|
||||
import be.vandewalleh.auth.SimpleJWT
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import be.vandewalleh.extensions.RoutingBuilder
|
||||
import be.vandewalleh.factories.configurationFactory
|
||||
import be.vandewalleh.factories.dataSourceFactory
|
||||
import be.vandewalleh.factories.databaseFactory
|
||||
import be.vandewalleh.factories.simpleJwtFactory
|
||||
import be.vandewalleh.features.*
|
||||
import be.vandewalleh.routing.NoteRoutes
|
||||
import be.vandewalleh.routing.TagRoutes
|
||||
import be.vandewalleh.routing.UserRoutes
|
||||
import be.vandewalleh.services.NoteService
|
||||
import be.vandewalleh.services.UserService
|
||||
import org.kodein.di.*
|
||||
import org.kodein.di.DI
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
val mainModule = DI.Module("main") {
|
||||
bind() from singleton { NoteService(instance()) }
|
||||
bind() from singleton { UserService(instance(), instance()) }
|
||||
|
||||
bind() from singleton { configurationFactory() }
|
||||
|
||||
bind() from setBinding<ApplicationBuilder>()
|
||||
bind<ApplicationBuilder>().inSet() with singleton { ErrorHandler() }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { ContentNegotiationFeature() }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { CorsFeature(instance<Config>().server.cors) }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { AuthenticationModule(instance(tag = "auth")) }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { MigrationHook(instance()) }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { ShutdownDatabaseConnection(instance()) }
|
||||
|
||||
bind() from setBinding<RoutingBuilder>()
|
||||
bind<RoutingBuilder>().inSet() with singleton { NoteRoutes(instance()) }
|
||||
bind<RoutingBuilder>().inSet() with singleton { TagRoutes(instance()) }
|
||||
bind<RoutingBuilder>().inSet() with singleton {
|
||||
UserRoutes(
|
||||
instance(tag = "auth"),
|
||||
instance(tag = "refresh"),
|
||||
instance(),
|
||||
instance()
|
||||
)
|
||||
}
|
||||
|
||||
bind<SimpleJWT>(tag = "auth") with singleton { simpleJwtFactory(instance<Config>().jwt.auth) }
|
||||
bind<SimpleJWT>(tag = "refresh") with singleton { simpleJwtFactory(instance<Config>().jwt.refresh) }
|
||||
|
||||
bind() from singleton { LoggerFactory.getLogger("Application") }
|
||||
bind() from singleton { dataSourceFactory(instance<Config>().database) }
|
||||
bind() from singleton { databaseFactory(instance()) }
|
||||
bind() from singleton { Migration(instance()) }
|
||||
|
||||
bind<PasswordHash>() with singleton { BcryptPasswordHash() }
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
package be.vandewalleh.auth
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.auth.jwt.*
|
||||
|
||||
class AuthenticationModule(authJwt: SimpleJWT) : ApplicationBuilder({
|
||||
install(Authentication) {
|
||||
jwt {
|
||||
verifier(authJwt.verifier)
|
||||
validate {
|
||||
UserIdPrincipal(it.payload.getClaim("id").asInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -1,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,20 +0,0 @@
|
||||
package be.vandewalleh.extensions
|
||||
|
||||
import be.vandewalleh.auth.UserIdPrincipal
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.response.*
|
||||
|
||||
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
|
||||
respondText(
|
||||
"""{"status": "${status.description}"}""",
|
||||
status = status,
|
||||
contentType = ContentType.Application.Json
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the userId for the currently authenticated user
|
||||
*/
|
||||
fun ApplicationCall.authenticatedUserId() = principal<UserIdPrincipal>()!!.id
|
||||
@ -1,95 +0,0 @@
|
||||
package be.vandewalleh.routing
|
||||
|
||||
import be.vandewalleh.extensions.RoutingBuilder
|
||||
import be.vandewalleh.extensions.authenticatedUserId
|
||||
import be.vandewalleh.extensions.respondStatus
|
||||
import be.vandewalleh.features.ValidationException
|
||||
import be.vandewalleh.services.NoteService
|
||||
import be.vandewalleh.validation.noteValidator
|
||||
import be.vandewalleh.validation.receiveValidated
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import java.util.*
|
||||
|
||||
class NoteRoutes(noteService: NoteService) : RoutingBuilder({
|
||||
authenticate {
|
||||
route("/notes") {
|
||||
createNote(noteService)
|
||||
getAllNotes(noteService)
|
||||
|
||||
route("/{uuid}") {
|
||||
getNote(noteService)
|
||||
updateNote(noteService)
|
||||
deleteNote(noteService)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
private fun Route.createNote(noteService: NoteService) {
|
||||
post {
|
||||
val userId = call.authenticatedUserId()
|
||||
val note = call.receiveValidated(noteValidator)
|
||||
val createdNote = noteService.create(userId, note)
|
||||
call.respond(HttpStatusCode.Created, createdNote)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.getAllNotes(noteService: NoteService) {
|
||||
get {
|
||||
val userId = call.authenticatedUserId()
|
||||
val limit = call.parameters["limit"]?.toInt() ?: 20 // FIXME validate
|
||||
val after = call.parameters["after"]?.let { UUID.fromString(it) } // FIXME validate
|
||||
val notes = noteService.findAll(userId, limit, after)
|
||||
call.respond(notes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.getNote(noteService: NoteService) {
|
||||
get {
|
||||
val userId = call.authenticatedUserId()
|
||||
val noteUuid = call.noteUuid()
|
||||
|
||||
val response = noteService.find(userId, noteUuid)
|
||||
?: return@get call.respondStatus(HttpStatusCode.NotFound)
|
||||
call.respond(response)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.updateNote(noteService: NoteService) {
|
||||
put {
|
||||
val userId = call.authenticatedUserId()
|
||||
val noteUuid = call.noteUuid()
|
||||
|
||||
val note = call.receiveValidated(noteValidator)
|
||||
note.uuid = noteUuid
|
||||
|
||||
if (noteService.updateNote(userId, note))
|
||||
call.respondStatus(HttpStatusCode.OK)
|
||||
else call.respondStatus(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.deleteNote(noteService: NoteService) {
|
||||
delete {
|
||||
val userId = call.authenticatedUserId()
|
||||
val noteUuid = call.noteUuid()
|
||||
|
||||
if (noteService.delete(userId, noteUuid))
|
||||
call.respondStatus(HttpStatusCode.OK)
|
||||
else
|
||||
call.respondStatus(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ApplicationCall.noteUuid(): UUID {
|
||||
val uuid = parameters["uuid"]
|
||||
return try {
|
||||
UUID.fromString(uuid)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw ValidationException("`$uuid` is not a valid UUID")
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
package be.vandewalleh.routing
|
||||
|
||||
import be.vandewalleh.extensions.RoutingBuilder
|
||||
import be.vandewalleh.extensions.authenticatedUserId
|
||||
import be.vandewalleh.services.NoteService
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
|
||||
class TagRoutes(noteService: NoteService) : RoutingBuilder({
|
||||
authenticate {
|
||||
get("/tags") {
|
||||
call.respond(noteService.getTags(call.authenticatedUserId()))
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -1,123 +0,0 @@
|
||||
package be.vandewalleh.routing
|
||||
|
||||
import be.vandewalleh.auth.SimpleJWT
|
||||
import be.vandewalleh.auth.UsernamePasswordCredential
|
||||
import be.vandewalleh.extensions.RoutingBuilder
|
||||
import be.vandewalleh.extensions.authenticatedUserId
|
||||
import be.vandewalleh.extensions.respondStatus
|
||||
import be.vandewalleh.features.PasswordHash
|
||||
import be.vandewalleh.services.UserService
|
||||
import be.vandewalleh.validation.receiveValidated
|
||||
import be.vandewalleh.validation.registerValidator
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
|
||||
class UserRoutes(
|
||||
authJWT: SimpleJWT,
|
||||
refreshJWT: SimpleJWT,
|
||||
userService: UserService,
|
||||
passwordHash: PasswordHash
|
||||
) : RoutingBuilder({
|
||||
route("/user") {
|
||||
createUser(userService)
|
||||
route("/login") {
|
||||
login(userService, passwordHash, authJWT, refreshJWT)
|
||||
}
|
||||
route("/refresh_token") {
|
||||
refreshToken(userService, authJWT, refreshJWT)
|
||||
}
|
||||
authenticate {
|
||||
deleteUser(userService)
|
||||
route("/me") {
|
||||
userInfo(userService)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
private fun Route.userInfo(userService: UserService) {
|
||||
get {
|
||||
val id = call.authenticatedUserId()
|
||||
val info = userService.find(id)
|
||||
if (info != null) call.respond(mapOf("user" to info))
|
||||
else call.respondStatus(HttpStatusCode.Unauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.deleteUser(userService: UserService) {
|
||||
delete {
|
||||
val userId = call.authenticatedUserId()
|
||||
call.respondStatus(
|
||||
if (userService.delete(userId)) HttpStatusCode.OK
|
||||
else HttpStatusCode.NotFound
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.createUser(userService: UserService) {
|
||||
post {
|
||||
val user = call.receiveValidated(registerValidator)
|
||||
|
||||
if (userService.exists(user.username))
|
||||
return@post call.respondStatus(HttpStatusCode.Conflict)
|
||||
|
||||
val newUser = userService.create(user.username, user.password)
|
||||
?: return@post call.respondStatus(HttpStatusCode.Conflict)
|
||||
|
||||
call.respond(HttpStatusCode.Created, newUser)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.login(
|
||||
userService: UserService,
|
||||
passwordHash: PasswordHash,
|
||||
authJWT: SimpleJWT,
|
||||
refreshJWT: SimpleJWT
|
||||
) {
|
||||
post {
|
||||
val credential = call.receive<UsernamePasswordCredential>()
|
||||
|
||||
val user = userService.find(credential.username)
|
||||
?: return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
||||
|
||||
if (!passwordHash.verify(credential.password, user.password)) {
|
||||
return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
||||
}
|
||||
|
||||
val response = DualToken(
|
||||
token = authJWT.sign(user.id),
|
||||
refreshToken = refreshJWT.sign(user.id)
|
||||
)
|
||||
return@post call.respond(response)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Route.refreshToken(userService: UserService, authJWT: SimpleJWT, refreshJWT: SimpleJWT) {
|
||||
post {
|
||||
val token = call.receive<RefreshToken>().refreshToken
|
||||
|
||||
val id = try {
|
||||
val decodedJWT = refreshJWT.verifier.verify(token)
|
||||
decodedJWT.getClaim("id").asInt()
|
||||
} catch (e: JWTVerificationException) {
|
||||
return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
||||
}
|
||||
|
||||
if (!userService.exists(id))
|
||||
return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
||||
|
||||
val response = DualToken(
|
||||
token = authJWT.sign(id),
|
||||
refreshToken = refreshJWT.sign(id)
|
||||
)
|
||||
return@post call.respond(response)
|
||||
}
|
||||
}
|
||||
|
||||
private data class RefreshToken(val refreshToken: String)
|
||||
private data class DualToken(val token: String, val refreshToken: String)
|
||||
98
css/styles.css
Normal file
98
css/styles.css
Normal file
@ -0,0 +1,98 @@
|
||||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
.nav {
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.centered {
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
#note a {
|
||||
@apply text-blue-700 underline; }
|
||||
|
||||
#note p {
|
||||
@apply my-4; }
|
||||
|
||||
#note blockquote,
|
||||
#note figure {
|
||||
@apply my-4 mx-10; }
|
||||
|
||||
#note hr {
|
||||
@apply border; }
|
||||
|
||||
#note h1 {
|
||||
@apply text-4xl font-bold my-2; }
|
||||
|
||||
#note h2 {
|
||||
@apply text-2xl font-bold my-3; }
|
||||
|
||||
#note h3 {
|
||||
@apply text-lg font-bold my-4; }
|
||||
|
||||
#note h4 {
|
||||
@apply text-base font-bold my-5; }
|
||||
|
||||
#note h5 {
|
||||
@apply text-sm font-bold my-6; }
|
||||
|
||||
#note h6 {
|
||||
@apply text-xs font-bold my-10; }
|
||||
|
||||
#note ul,
|
||||
#note menu {
|
||||
@apply list-disc my-1 pl-10; }
|
||||
|
||||
#note ol {
|
||||
@apply list-decimal my-4 pl-10; }
|
||||
|
||||
#note ul ul,
|
||||
#note ol ul {
|
||||
list-style-type: circle; }
|
||||
|
||||
#note ul ul ul,
|
||||
#note ul ol ul,
|
||||
#note ol ul ul,
|
||||
#note ol ol ul {
|
||||
list-style-type: square; }
|
||||
|
||||
#note dd {
|
||||
@apply pl-10; }
|
||||
|
||||
#note dl {
|
||||
@apply my-4; }
|
||||
|
||||
#note ul ul,
|
||||
#note ul ol,
|
||||
#note ul menu,
|
||||
#note ul dl,
|
||||
#note ol ul,
|
||||
#note ol ol,
|
||||
#note ol menu,
|
||||
#note ol dl,
|
||||
#note menu ul,
|
||||
#note menu ol,
|
||||
#note menu menu,
|
||||
#note menu dl,
|
||||
#note dl ul,
|
||||
#note dl ol,
|
||||
#note dl menu,
|
||||
#note dl dl {
|
||||
margin: 0; }
|
||||
|
||||
#note legend {
|
||||
@apply py-0 px-1; }
|
||||
|
||||
#note fieldset {
|
||||
@apply my-0 mx-1 pt-0 px-1 pb-2; }
|
||||
|
||||
#note b,
|
||||
#note strong {
|
||||
font-weight: bold; }
|
||||
|
||||
#note pre {
|
||||
@apply my-4 rounded-md overflow-x-auto p-2 block font-mono bg-gray-700; }
|
||||
|
||||
@tailwind utilities;
|
||||
@ -1,13 +0,0 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@ -1,22 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
extends: [
|
||||
'@nuxtjs',
|
||||
'prettier',
|
||||
'prettier/vue',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:nuxt/recommended'
|
||||
],
|
||||
plugins: [
|
||||
'prettier'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {}
|
||||
}
|
||||
90
frontend/.gitignore
vendored
90
frontend/.gitignore
vendored
@ -1,90 +0,0 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.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
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE / Editor
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Vim swap files
|
||||
*.swp
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"arrowParens": "always",
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
:7000
|
||||
|
||||
log {
|
||||
format single_field common_log
|
||||
}
|
||||
|
||||
encode gzip
|
||||
|
||||
root * dist
|
||||
file_server
|
||||
|
||||
try_files {path} /200.html
|
||||
|
||||
@nuxt {
|
||||
path /_nuxt/*
|
||||
}
|
||||
|
||||
header @nuxt Cache-Control "public,max-age=31536000,immutable" # 1 year
|
||||
@ -1,3 +0,0 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
@ -1,6 +0,0 @@
|
||||
.page-enter-active, .page-leave-active {
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.page-enter, .page-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div class="h-screen font-sans text-gray-100">
|
||||
<nuxt />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
html {
|
||||
@apply bg-gray-900;
|
||||
}
|
||||
</style>
|
||||
@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<nuxt />
|
||||
</template>
|
||||
@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="h-screen container mx-auto h-full flex justify-center items-center"
|
||||
>
|
||||
<div
|
||||
class="text-gray-200 w-full md:w-1/2 lg:w-1/3 bg-gray-800 border-teal-500 p-8 border-t-8 bg-white mb-6 rounded-lg shadow-lg"
|
||||
>
|
||||
<h1 v-if="error.statusCode === 404">
|
||||
{{ pageNotFound }}
|
||||
</h1>
|
||||
<h1 v-else>
|
||||
{{ otherError }}
|
||||
</h1>
|
||||
<NuxtLink to="/" class="text-blue-200 underline">
|
||||
Home page
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'empty',
|
||||
props: {
|
||||
error: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageNotFound: '404 Not Found',
|
||||
otherError: 'An error occurred',
|
||||
}
|
||||
},
|
||||
head() {
|
||||
const title =
|
||||
this.error.statusCode === 404 ? this.pageNotFound : this.otherError
|
||||
return {
|
||||
title,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1,153 +0,0 @@
|
||||
export default ({ command }) => ({
|
||||
mode: 'universal',
|
||||
/*
|
||||
** Headers of the page
|
||||
*/
|
||||
head: {
|
||||
titleTemplate: '%s - ' + 'SimpleNotes',
|
||||
title: 'SimpleNotes' || '',
|
||||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
},
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1',
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: process.env.npm_package_description || '',
|
||||
},
|
||||
],
|
||||
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
|
||||
},
|
||||
/*
|
||||
** Customize the progress-bar color
|
||||
*/
|
||||
loading: { color: '#fff' },
|
||||
/*
|
||||
** Global CSS
|
||||
*/
|
||||
css: ['~/assets/main.css'],
|
||||
/*
|
||||
** Plugins to load before mounting the App
|
||||
*/
|
||||
plugins: [],
|
||||
/*
|
||||
** Nuxt.js dev-modules
|
||||
*/
|
||||
buildModules: [
|
||||
// Doc: https://github.com/nuxt-community/eslint-module
|
||||
'@nuxtjs/eslint-module',
|
||||
'@nuxtjs/tailwindcss',
|
||||
],
|
||||
/*
|
||||
** Nuxt.js modules
|
||||
*/
|
||||
modules: [
|
||||
// Doc: https://axios.nuxtjs.org/usage
|
||||
'@nuxtjs/axios',
|
||||
// Doc: https://github.com/nuxt-community/dotenv-module
|
||||
'@nuxtjs/auth',
|
||||
// Doc: https://github.com/nuxt-community/robots-module
|
||||
'@nuxtjs/robots',
|
||||
],
|
||||
/*
|
||||
** Axios module configuration
|
||||
** See https://axios.nuxtjs.org/options
|
||||
*/
|
||||
axios: {},
|
||||
|
||||
publicRuntimeConfig: {
|
||||
API_HOST: process.env.API_HOST,
|
||||
},
|
||||
|
||||
auth: {
|
||||
redirect: {
|
||||
login: '/signin',
|
||||
logout: '/',
|
||||
home: '/notes',
|
||||
},
|
||||
watchLoggedIn: true,
|
||||
cookie: !command.includes('generate'),
|
||||
strategies: {
|
||||
_scheme: 'local',
|
||||
_name: 'local',
|
||||
local: {
|
||||
endpoints: {
|
||||
login: {
|
||||
url: '/user/login',
|
||||
method: 'post',
|
||||
propertyName: 'token',
|
||||
},
|
||||
user: {
|
||||
url: '/user/me',
|
||||
method: 'get',
|
||||
propertyName: 'user',
|
||||
},
|
||||
logout: false,
|
||||
},
|
||||
autoFetchUser: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
router: {
|
||||
middleware: ['auth'],
|
||||
},
|
||||
|
||||
/*
|
||||
** Build configuration
|
||||
*/
|
||||
build: {
|
||||
extractCSS: false,
|
||||
|
||||
terser: {
|
||||
parallel: true,
|
||||
cache: false,
|
||||
sourceMap: false,
|
||||
extractComments: false,
|
||||
terserOptions: {},
|
||||
},
|
||||
|
||||
html: {
|
||||
minify: {
|
||||
collapseBooleanAttributes: true,
|
||||
decodeEntities: true,
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
processConditionalComments: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeRedundantAttributes: true,
|
||||
trimCustomFragments: true,
|
||||
useShortDoctype: true,
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
** You can extend webpack config here
|
||||
*/
|
||||
extend(config, ctx) {},
|
||||
},
|
||||
|
||||
components: true,
|
||||
|
||||
telemetry: false,
|
||||
|
||||
robots: {
|
||||
UserAgent: '*',
|
||||
Disallow: '/',
|
||||
},
|
||||
|
||||
render: {
|
||||
bundleRenderer: {
|
||||
shouldPrefetch: () => false,
|
||||
shouldPreload: (_, asType) =>
|
||||
['font', 'script', 'style'].includes(asType),
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -1,41 +0,0 @@
|
||||
{
|
||||
"name": "Notes",
|
||||
"version": "1.0.0",
|
||||
"description": "Simple note taking and sharing website",
|
||||
"author": "Hubert Van De Walle",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nuxt",
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start",
|
||||
"generate": "nuxt generate --modern",
|
||||
"serve": "caddy run",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/auth": "^4.9.1",
|
||||
"@nuxtjs/axios": "^5.3.6",
|
||||
"@nuxtjs/robots": "^2.4.2",
|
||||
"@starptech/prettyhtml-hast-to-html": "^0.10.0",
|
||||
"nuxt": "^2.13.0",
|
||||
"remark": "^12.0.0",
|
||||
"remark-breaks": "^1.0.5",
|
||||
"remark-parse": "^8.0.2",
|
||||
"remark-rehype": "^7.0.0",
|
||||
"timeago.js": "^4.0.2",
|
||||
"unified": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdi/js": "^5.1.45",
|
||||
"@nuxtjs/eslint-config": "^2.0.2",
|
||||
"@nuxtjs/eslint-module": "^1.1.0",
|
||||
"@nuxtjs/tailwindcss": "^2.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-nuxt": "^0.5.2",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"prettier": "^2.0.5"
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li
|
||||
v-for="link in links"
|
||||
:key="link.name"
|
||||
class="text-blue-200"
|
||||
>
|
||||
<nuxt-link :to="link.url" class="underline">{{
|
||||
link.name
|
||||
}}</nuxt-link>
|
||||
</li>
|
||||
<li class="text-blue-200">
|
||||
<button class="underline" @click="logout">
|
||||
Sign Out
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<client-only>
|
||||
<div v-if="$auth.$state.loggedIn">
|
||||
User: {{ $auth.$state.user }}
|
||||
</div>
|
||||
</client-only>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Home',
|
||||
data: () => ({
|
||||
links: [
|
||||
{ url: '/signin', name: 'Sign In' },
|
||||
{ url: '/register', name: 'Register' },
|
||||
{ url: '/notes', name: 'Notes' },
|
||||
{ url: '/404', name: '404' },
|
||||
],
|
||||
}),
|
||||
options: {
|
||||
auth: false,
|
||||
},
|
||||
methods: {
|
||||
async logout() {
|
||||
this.$store.commit('notes/clear')
|
||||
await this.$auth.logout()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>{{ uuid }}</div>
|
||||
<div v-if="note">{{ note }}</div>
|
||||
<!-- html is sanitized -->
|
||||
<!-- eslint-disable-next-line -->
|
||||
<div class="container mx-auto" id="note" v-html="html"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions, mapGetters } from 'vuex'
|
||||
import renderMarkdown from '@/utils/markdown'
|
||||
|
||||
export default {
|
||||
name: 'Note',
|
||||
data: () => ({
|
||||
note: {},
|
||||
html: '',
|
||||
}),
|
||||
computed: {
|
||||
...mapState('notes', ['notes', 'isInitialized']),
|
||||
...mapGetters('notes', ['find']),
|
||||
uuid() {
|
||||
return this.$route.params.uuid
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (!this.isInitialized) {
|
||||
this.load().then(() => {
|
||||
this.note = this.find(this.uuid)
|
||||
this.render()
|
||||
})
|
||||
} else {
|
||||
this.note = this.find(this.uuid)
|
||||
this.render()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('notes', ['load']),
|
||||
render() {
|
||||
this.html = renderMarkdown(this.note.content).contents
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#note h1 {
|
||||
@apply text-5xl font-bold;
|
||||
}
|
||||
</style>
|
||||
@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<div class="container p-4 mx-auto bg-gray-800">
|
||||
<h1 class="text-center">My Notes</h1>
|
||||
<nuxt-link
|
||||
to="/notes/new"
|
||||
class="bg-blue-700 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded"
|
||||
>New note</nuxt-link
|
||||
>
|
||||
<input
|
||||
id="search"
|
||||
name="search"
|
||||
class="block p-2 my-2 appearance-none w-full bg-transparent border-white-200 border-b focus:border-red-500"
|
||||
placeholder="search"
|
||||
aria-label="search"
|
||||
/>
|
||||
<div v-for="note in notes" :key="note.uuid">
|
||||
<div class="flex justify-between items-center">
|
||||
<nuxt-link :to="'/notes/' + note.uuid" class="text-lg">{{
|
||||
note.title
|
||||
}}</nuxt-link>
|
||||
<div class="py-2">
|
||||
<span
|
||||
v-for="(tag, index) in note.tags"
|
||||
:key="index"
|
||||
class="inline-block text-sm bg-gray-500 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 ml-2"
|
||||
>{{ tag }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Notes',
|
||||
computed: {
|
||||
...mapState('notes', ['notes', 'isInitialized']),
|
||||
},
|
||||
mounted() {
|
||||
if (!this.isInitialized) this.load()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('notes', ['load']),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div class="container mx-auto">
|
||||
<form @submit.prevent="submit">
|
||||
<textarea
|
||||
v-model="content"
|
||||
aria-label="markdown input"
|
||||
name="content"
|
||||
rows="30"
|
||||
style="outline: none !important;"
|
||||
class="w-full bg-gray-800 p-5"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
<button
|
||||
class="block bg-blue-700 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'New',
|
||||
data: () => ({
|
||||
title: 'Test',
|
||||
tags: [],
|
||||
content: '',
|
||||
}),
|
||||
methods: {
|
||||
...mapActions('notes', ['create']),
|
||||
submit() {
|
||||
this.create({
|
||||
title: this.title,
|
||||
tags: this.tags,
|
||||
content: this.content,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,127 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="h-screen container mx-auto h-full flex justify-center items-center"
|
||||
>
|
||||
<div class="w-full md:w-1/2 lg:w-1/3">
|
||||
<h1 class="font-semibold text-lg mb-6 text-center">
|
||||
Create an Account
|
||||
</h1>
|
||||
<div
|
||||
class="bg-gray-800 border-teal-500 p-8 border-t-8 bg-white mb-6 rounded-lg shadow-lg"
|
||||
>
|
||||
<form @submit.prevent="register">
|
||||
<div
|
||||
v-if="showError"
|
||||
class="bg-red-500 border-l-4 border-red-700 text-red-200 p-4 mb-4"
|
||||
role="alert"
|
||||
>
|
||||
<p class="font-bold">Error</p>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="username"
|
||||
class="font-bold text-grey-darker block mb-2"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
name="username"
|
||||
class="shadow focus:shadow-outline block appearance-none w-full bg-gray-700 border-gray-500 hover:border-gray-500 px-2 py-2 rounded shadow"
|
||||
placeholder="Your Username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="password"
|
||||
class="font-bold text-grey-darker block mb-2"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
name="password"
|
||||
type="password"
|
||||
class="shadow focus:shadow-outline block appearance-none w-full bg-gray-700 border-gray-500 hover:border-gray-500 px-2 py-2 rounded shadow"
|
||||
placeholder="Your Password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="confirm"
|
||||
class="font-bold text-grey-darker block mb-2"
|
||||
>
|
||||
Confirm your password
|
||||
</label>
|
||||
<input
|
||||
id="confirm"
|
||||
v-model="confirm"
|
||||
name="confirm"
|
||||
type="password"
|
||||
class="shadow focus:shadow-outline block appearance-none w-full bg-gray-700 border-gray-500 hover:border-gray-500 px-2 py-2 rounded shadow"
|
||||
placeholder="Confirm your Password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-teal-500 hover:bg-teal-400 text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-gray-200 text-sm">
|
||||
Already have an account?
|
||||
<nuxt-link
|
||||
to="/signin"
|
||||
class="no-underline text-blue-500 font-bold"
|
||||
>Sign in
|
||||
</nuxt-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SignIn',
|
||||
options: {
|
||||
auth: false, // FIXME: auth: 'guest'
|
||||
},
|
||||
data: () => ({
|
||||
confirm: '',
|
||||
form: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
error: '',
|
||||
showError: false,
|
||||
}),
|
||||
methods: {
|
||||
register() {
|
||||
this.$axios
|
||||
.post('/user', this.form)
|
||||
.then(() => this.$router.push('/signin'))
|
||||
.catch((e) => {
|
||||
if (e.response) {
|
||||
this.error = e.response.data.error
|
||||
this.showError = true
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
head: () => ({
|
||||
title: 'Sign in',
|
||||
}),
|
||||
}
|
||||
</script>
|
||||
@ -1,93 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="h-screen container mx-auto h-full flex justify-center items-center"
|
||||
>
|
||||
<div class="w-full md:w-1/2 lg:w-1/3">
|
||||
<h1 class="font-semibold text-lg mb-6 text-center">Sign In</h1>
|
||||
<div
|
||||
class="bg-gray-800 border-teal-500 p-8 border-t-8 bg-white mb-6 rounded-lg shadow-lg"
|
||||
>
|
||||
<form @submit.prevent="userLogin">
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="username"
|
||||
class="font-bold text-grey-darker block mb-2"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
name="username"
|
||||
class="shadow focus:shadow-outline block appearance-none w-full bg-gray-700 border-gray-500 hover:border-gray-500 px-2 py-2 rounded shadow"
|
||||
placeholder="Your Username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="password"
|
||||
class="font-bold text-grey-darker block mb-2"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
name="password"
|
||||
type="password"
|
||||
class="shadow focus:shadow-outline block appearance-none w-full bg-gray-700 border-gray-500 hover:border-gray-500 px-2 py-2 rounded shadow"
|
||||
placeholder="Your Password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-teal-500 hover:bg-teal-400 text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-gray-200 text-sm">
|
||||
Don't have an account?
|
||||
<nuxt-link
|
||||
to="/register"
|
||||
class="no-underline text-blue-500 font-bold"
|
||||
>Create an Account</nuxt-link
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SignIn',
|
||||
options: {
|
||||
auth: false, // FIXME: auth: 'guest'
|
||||
},
|
||||
data: () => ({
|
||||
form: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
}),
|
||||
methods: {
|
||||
userLogin() {
|
||||
this.$auth
|
||||
.loginWith('local', {
|
||||
data: this.form,
|
||||
})
|
||||
.then(() => this.$router.push('/'))
|
||||
},
|
||||
},
|
||||
head: () => ({
|
||||
title: 'Sign in',
|
||||
}),
|
||||
}
|
||||
</script>
|
||||
@ -1,7 +0,0 @@
|
||||
export const state = () => ({})
|
||||
|
||||
export const mutations = {}
|
||||
|
||||
export const actions = {}
|
||||
|
||||
export const getters = {}
|
||||
@ -1,53 +0,0 @@
|
||||
export const state = () => ({
|
||||
notes: [],
|
||||
isInitialized: false,
|
||||
})
|
||||
|
||||
export const mutations = {
|
||||
setInitialized(state) {
|
||||
state.isInitialized = true
|
||||
},
|
||||
set(state, notes) {
|
||||
state.notes = notes
|
||||
},
|
||||
add(state, note) {
|
||||
state.notes.unshift(note)
|
||||
},
|
||||
delete(state, uuid) {
|
||||
state.notes = state.notes.filter((e) => e.uuid !== uuid)
|
||||
},
|
||||
// used when logging out
|
||||
clear(state) {
|
||||
state.notes = []
|
||||
state.initialized = false
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
load({ commit }) {
|
||||
return this.$axios.get('/notes').then(({ data }) => {
|
||||
commit('set', data)
|
||||
commit('setInitialized')
|
||||
})
|
||||
},
|
||||
create({ commit }, note) {
|
||||
this.$axios.post('/notes', note).then(({ data }) =>
|
||||
commit('add', {
|
||||
uuid: data.uuid,
|
||||
title: data.title,
|
||||
updatedAt: data.updatedAt,
|
||||
tags: data.tags,
|
||||
})
|
||||
)
|
||||
},
|
||||
delete({ commit }, note) {
|
||||
const result = this.$axios.delete(`/notes/${note.uuid}`)
|
||||
commit('delete', note.uuid)
|
||||
result.catch(() => commit('add', note))
|
||||
},
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
isEmpty: (state) => state.isInitialized && state.notes.length === 0,
|
||||
find: (state) => (uuid) => state.notes.find((note) => note.uuid === uuid),
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
/*
|
||||
** TailwindCSS Configuration File
|
||||
**
|
||||
** Docs: https://tailwindcss.com/docs/configuration
|
||||
** Default: https://github.com/tailwindcss/tailwindcss/blob/master/stubs/defaultConfig.stub.js
|
||||
*/
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
gray: {
|
||||
100: '#F8F9FA',
|
||||
200: '#EBEBEB',
|
||||
300: '#DEE2E6',
|
||||
400: '#CED4DA',
|
||||
500: '#ADB5BD',
|
||||
600: '#888',
|
||||
700: '#444',
|
||||
800: '#303030',
|
||||
900: '#222',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {},
|
||||
plugins: [],
|
||||
purge: {
|
||||
// Learn more on https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css
|
||||
enabled: process.env.NODE_ENV === 'production',
|
||||
content: [
|
||||
'components/**/*.vue',
|
||||
'layouts/**/*.vue',
|
||||
'pages/**/*.vue',
|
||||
'plugins/**/*.js',
|
||||
'nuxt.config.js',
|
||||
],
|
||||
},
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
import markdown from 'remark-parse'
|
||||
import remark2rehype from 'remark-rehype'
|
||||
import remarkBreaks from 'remark-breaks'
|
||||
import unified from 'unified'
|
||||
import hast2html from '@starptech/prettyhtml-hast-to-html'
|
||||
|
||||
function stringify(options = {}) {
|
||||
this.Compiler = (tree) => hast2html(tree, options)
|
||||
}
|
||||
|
||||
const compiler = unified()
|
||||
.use(markdown)
|
||||
.use(remark2rehype)
|
||||
.use(remarkBreaks)
|
||||
.use(stringify)
|
||||
|
||||
export default function (input) {
|
||||
return compiler.processSync(input)
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
resolve: {
|
||||
// for IntelliJ
|
||||
alias: {
|
||||
// eslint-disable-next-line no-undef
|
||||
'@': path.resolve(__dirname),
|
||||
// eslint-disable-next-line no-undef
|
||||
'~': path.resolve(__dirname),
|
||||
},
|
||||
},
|
||||
}
|
||||
9328
frontend/yarn.lock
9328
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
0
api/mvnw → mvnw
vendored
0
api/mvnw → mvnw
vendored
0
api/mvnw.cmd → mvnw.cmd
vendored
0
api/mvnw.cmd → mvnw.cmd
vendored
15
package.json
Normal file
15
package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "css",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"css": "postcss build css/styles.css --output resources/static/styles.css",
|
||||
"doc": "aglio -i api-doc/api.apib -o resources/docs/index.html --theme-variables slate"
|
||||
},
|
||||
"dependencies": {
|
||||
"aglio": "^2.3.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-hash": "^2.0.0",
|
||||
"tailwindcss": "^1.5.1"
|
||||
}
|
||||
}
|
||||
@ -50,6 +50,16 @@
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>korlibs</id>
|
||||
<url>https://dl.bintray.com/korlibs/korlibs</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>jitpack</id>
|
||||
<url>https://jitpack.io</url>
|
||||
@ -62,6 +72,11 @@
|
||||
</repository>
|
||||
</repositories>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.soywiz.korlibs.korte</groupId>
|
||||
<artifactId>korte-jvm</artifactId>
|
||||
<version>1.10.14</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
@ -137,7 +152,12 @@
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<version>2.10.4</version>
|
||||
<version>2.11.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
<version>2.11.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.hekeki</groupId>
|
||||
@ -170,6 +190,18 @@
|
||||
<artifactId>yavi</artifactId>
|
||||
<version>0.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vladsch.flexmark</groupId>
|
||||
<artifactId>flexmark-all</artifactId>
|
||||
<version>0.62.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
|
||||
<artifactId>owasp-java-html-sanitizer</artifactId>
|
||||
<version>20200615.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- tests -->
|
||||
<dependency>
|
||||
<groupId>com.github.javafaker</groupId>
|
||||
<artifactId>javafaker</artifactId>
|
||||
18
postcss.config.js
Normal file
18
postcss.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
require('cssnano')({
|
||||
preset: ['default', {
|
||||
discardComments: {
|
||||
removeAll: true,
|
||||
},
|
||||
}]
|
||||
}),
|
||||
require('postcss-hash')({
|
||||
algorithm: 'sha256',
|
||||
trim: 20,
|
||||
manifest: 'resources/css-manifest.json'
|
||||
}),
|
||||
]
|
||||
}
|
||||
@ -8,7 +8,7 @@ database:
|
||||
server:
|
||||
host: ${HOST:-127.0.0.1}
|
||||
port: ${PORT:-8081}
|
||||
cors: ${CORS:-true}
|
||||
cors: ${CORS:-false}
|
||||
|
||||
jwt:
|
||||
auth:
|
||||
@ -12,7 +12,8 @@ create table Notes
|
||||
(
|
||||
uuid binary(16) not null primary key,
|
||||
title varchar(50) not null,
|
||||
content text not null,
|
||||
markdown mediumtext not null,
|
||||
html mediumtext not null,
|
||||
user_id int not null,
|
||||
updated_at datetime null,
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
<logger name="me.liuwj.ktorm.database" level="DEBUG"/>
|
||||
<logger name="me.liuwj.ktorm.database" level="INFO"/>
|
||||
<logger name="com.zaxxer.hikari" level="INFO"/>
|
||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||
<logger name="io.netty" level="INFO"/>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
2
resources/static/robots.txt
Normal file
2
resources/static/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
16
resources/templates/__base__.html
Normal file
16
resources/templates/__base__.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% import "__macros__.html" as macros %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="description" content="new note"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>{{ title }} - SimpleNotes</title>
|
||||
{{ macros.stylesheet("styles.css") }}
|
||||
</head>
|
||||
<body class="bg-gray-900 text-white">
|
||||
{% include "components/navbar.html" %}
|
||||
{% block content %}default content{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
5
resources/templates/__macros__.html
Normal file
5
resources/templates/__macros__.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% macro stylesheet(path) %}
|
||||
{% set path = styles(path) %}
|
||||
<link rel="preload" href="{{ path }}" as="style">
|
||||
<link rel="stylesheet" href="{{ path }}">
|
||||
{% endmacro %}
|
||||
8
resources/templates/_uuid.html
Normal file
8
resources/templates/_uuid.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% set title = note.title %} {% extends "__base__.html" %} {% block content %}
|
||||
|
||||
<div class="container mx-auto p-4">
|
||||
<h1 class="text-3xl underline mb-4">{{ note.title }}</h1>
|
||||
<div id="note">{{ note.html | raw }}</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
6
resources/templates/components/alert.html
Normal file
6
resources/templates/components/alert.html
Normal file
@ -0,0 +1,6 @@
|
||||
{% macro warning(title, warning) %}
|
||||
<div class="bg-red-500 border border-red-400 text-red-200 px-4 py-3 mb-4 rounded relative" role="alert">
|
||||
<strong class="font-bold">{{ title }}</strong>
|
||||
<span class="block sm:inline">{{ warning }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
27
resources/templates/components/forms.html
Normal file
27
resources/templates/components/forms.html
Normal file
@ -0,0 +1,27 @@
|
||||
<!-- {id, label, placeholder?, autocomplete?} -->
|
||||
{% macro input(args) %}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="{{ args.id }}"
|
||||
class="font-bold text-grey-darker block mb-2"
|
||||
>{{ args.label }}</label>
|
||||
<input
|
||||
id="{{ args.id }}"
|
||||
name="{{ args.id }}"
|
||||
{% if args.autocomplete %}autocomplete="{{ args.autocomplete }}"{% endif %}
|
||||
{% if args.placeholder %}placeholder="{{ args.placeholder }}"{% endif %}
|
||||
class="shadow focus:shadow-outline block appearance-none w-full bg-gray-700 border-gray-500 hover:border-gray-500 px-2 py-2 rounded shadow"
|
||||
/>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro submit(value) %}
|
||||
<div class="flex items-center mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-teal-500 hover:bg-teal-400 text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
{{ value }}
|
||||
</button>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
21
resources/templates/components/navbar.html
Normal file
21
resources/templates/components/navbar.html
Normal file
@ -0,0 +1,21 @@
|
||||
<nav
|
||||
class="nav bg-teal-700 shadow-md flex items-center justify-between px-4"
|
||||
>
|
||||
<a href="/" class="text-2xl text-gray-800 font-bold">SimpleNotes</a>
|
||||
<ul>
|
||||
{% if user %}
|
||||
<li class="inline text-gray-800 ml-2 text-md font-semibold">
|
||||
<a href="/notes">Notes</a>
|
||||
</li>
|
||||
<li class="inline text-gray-800 ml-2 text-md font-semibold bg-green-500 hover:bg-green-700 rounded px-4 py-2">
|
||||
<form class="inline" action="/logout" method="post">
|
||||
<button type="submit">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="inline text-gray-800 pl-2 text-md font-semibold">
|
||||
<a href="/login">Sign In</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
11
resources/templates/error.html
Normal file
11
resources/templates/error.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% set title = status %}
|
||||
{% extends "__base__.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="centered container mx-auto flex justify-center items-center">
|
||||
<div class="bg-gray-800 md:w-1/3 w-full rounded-lg m-4 p-6 text-center">
|
||||
<h1 class="text-3xl">Error</h1>
|
||||
<div class="text-red-400">{{ status }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
12
resources/templates/index.html
Normal file
12
resources/templates/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% set title = "Notes" %}
|
||||
{% extends "__base__.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="centered container mx-auto flex justify-center items-center">
|
||||
<div class="bg-gray-800 md:w-1/3 w-full rounded-lg m-4 p-6 text-center">
|
||||
<h1 class="text-3xl">SimpleNotes</h1>
|
||||
<div class="text-teal-400">Welcome</div>
|
||||
<div class="text-gray-200">TODO</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
24
resources/templates/list.html
Normal file
24
resources/templates/list.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% set title = "Notes" %}
|
||||
{% extends "__base__.html" %}
|
||||
{% block content %}
|
||||
<div class="container mx-auto p-4">
|
||||
<div class="flex justify-between">
|
||||
<h1 class="text-2xl underline">Notes</h1>
|
||||
<a
|
||||
href="/notes/new"
|
||||
class="inline text-gray-800 ml-2 text-md font-semibold bg-green-500 hover:bg-green-700 rounded px-4 py-2">New</a>
|
||||
</div>
|
||||
|
||||
{% if notes.size() > 0 -%}
|
||||
<ul>
|
||||
{% for note in notes -%}
|
||||
<li class="text-blue-200 hover:underline">
|
||||
<a href="/notes/{{ note.uuid }}">{{ note.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span>No notes :c</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
resources/templates/login.html
Normal file
40
resources/templates/login.html
Normal file
@ -0,0 +1,40 @@
|
||||
{% set title = "Sign In" %}
|
||||
{% extends "__base__.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="centered container mx-auto flex justify-center items-center">
|
||||
<div class="w-full md:w-1/2 lg:w-1/3 m-4">
|
||||
<h1 class="font-semibold text-lg mb-6 text-center">Sign In</h1>
|
||||
<div
|
||||
class="bg-gray-800 border-teal-500 p-8 border-t-8 bg-white mb-6 rounded-lg shadow-lg"
|
||||
>
|
||||
|
||||
{%- if error %}
|
||||
{% import "components/alert.html" as alerts %}
|
||||
{{ alerts.warning("Error", error) }}
|
||||
{% endif -%}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
|
||||
{% import "components/forms.html" as forms %}
|
||||
{{ forms.input({
|
||||
"id": "username", "label": "Username",
|
||||
"placeholder": "Your Username", "autocomplete": "username"
|
||||
}) }}
|
||||
{{ forms.input({
|
||||
"id": "password", "label": "Password",
|
||||
"placeholder": "Your Password", "autocomplete": "current-password"
|
||||
}) }}
|
||||
{{ forms.submit("Sign In") }}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-gray-200 text-sm">
|
||||
Don't have an account?
|
||||
<a href="/register" class="no-underline text-blue-500 font-bold">Create an Account</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
42
resources/templates/new.html
Normal file
42
resources/templates/new.html
Normal file
@ -0,0 +1,42 @@
|
||||
{% set title = "Notes" %} {% extends "__base__.html" %} {% block content %}
|
||||
|
||||
<div class="container mx-auto">
|
||||
<h1 class="text-2xl">{{ method }}</h1>
|
||||
|
||||
{%- if error %}
|
||||
{% import "components/alert.html" as alerts %}
|
||||
<div class="mt-4">
|
||||
{{ alerts.warning("Error", error) }}
|
||||
</div>
|
||||
{% endif -%}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<textarea
|
||||
id="markdown"
|
||||
name="markdown"
|
||||
aria-label="markdown input"
|
||||
rows="20"
|
||||
class="w-full bg-gray-800 p-5 outline-none font-mono"
|
||||
spellcheck="false"
|
||||
>
|
||||
{%- if value %}{{ value }}{% else %}
|
||||
---
|
||||
title: ''
|
||||
---
|
||||
|
||||
{% endif -%}</textarea
|
||||
>
|
||||
|
||||
<div class="mt-2">
|
||||
<button
|
||||
class="bg-green-500 hover:bg-green-700 rounded px-4 py-2"
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p>{{ value }}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
42
resources/templates/register.html
Normal file
42
resources/templates/register.html
Normal file
@ -0,0 +1,42 @@
|
||||
{% set title = "Create an Account" %}
|
||||
{% extends "__base__.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="centered container mx-auto flex justify-center items-center">
|
||||
<div class="w-full md:w-1/2 lg:w-1/3 m-4">
|
||||
<h1 class="font-semibold text-lg mb-6 text-center">Create an Account</h1>
|
||||
<div
|
||||
class="bg-gray-800 border-teal-500 p-8 border-t-8 bg-white mb-6 rounded-lg shadow-lg"
|
||||
>
|
||||
|
||||
{%- if error %}
|
||||
{% import "components/alert.html" as alerts %}
|
||||
{% for e in error %}
|
||||
{{ alerts.warning("Error", e) }}
|
||||
{% endfor %}
|
||||
{% endif -%}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
|
||||
{% import "components/forms.html" as forms %}
|
||||
{{ forms.input({
|
||||
"id": "username", "label": "Username",
|
||||
"placeholder": "Your Username", "autocomplete": "username"
|
||||
}) }}
|
||||
{{ forms.input({
|
||||
"id": "password", "label": "Password",
|
||||
"placeholder": "Your Password", "autocomplete": "current-password"
|
||||
}) }}
|
||||
{{ forms.submit("Sign In") }}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-gray-200 text-sm">
|
||||
Already have an account?
|
||||
<a href="/login" class="no-underline text-blue-500 font-bold">Sign In</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
100
src/Dependencies.kt
Normal file
100
src/Dependencies.kt
Normal file
@ -0,0 +1,100 @@
|
||||
package be.vandewalleh
|
||||
|
||||
import be.vandewalleh.auth.AuthenticationModule
|
||||
import be.vandewalleh.auth.SimpleJWT
|
||||
import be.vandewalleh.controllers.api.ApiNoteController
|
||||
import be.vandewalleh.controllers.api.ApiTagController
|
||||
import be.vandewalleh.controllers.api.ApiUserController
|
||||
import be.vandewalleh.controllers.web.BaseController
|
||||
import be.vandewalleh.controllers.web.NoteController
|
||||
import be.vandewalleh.controllers.web.UserController
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import be.vandewalleh.extensions.RoutingBuilder
|
||||
import be.vandewalleh.factories.*
|
||||
import be.vandewalleh.features.*
|
||||
import be.vandewalleh.markdown.Markdown
|
||||
import be.vandewalleh.repositories.NoteRepository
|
||||
import be.vandewalleh.repositories.UserRepository
|
||||
import be.vandewalleh.routing.api.ApiDocRoutes
|
||||
import be.vandewalleh.routing.api.ApiNoteRoutes
|
||||
import be.vandewalleh.routing.api.ApiTagRoutes
|
||||
import be.vandewalleh.routing.api.ApiUserRoutes
|
||||
import be.vandewalleh.routing.web.BaseRoutes
|
||||
import be.vandewalleh.routing.web.NoteRoutes
|
||||
import be.vandewalleh.routing.web.StaticRoutes
|
||||
import be.vandewalleh.routing.web.UserRoutes
|
||||
import com.soywiz.korte.TemplateProvider
|
||||
import com.soywiz.korte.Templates
|
||||
import io.ktor.features.*
|
||||
import org.kodein.di.*
|
||||
import org.kodein.di.DI
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
val mainModule = DI.Module("main") {
|
||||
bind() from singleton { NoteRepository(instance()) }
|
||||
bind() from singleton { UserRepository(instance(), instance()) }
|
||||
|
||||
bind() from singleton { configurationFactory() }
|
||||
|
||||
bind() from setBinding<ApplicationBuilder>()
|
||||
bind<ApplicationBuilder>().inSet() with singleton { ErrorHandler(instance()) }
|
||||
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<ApplicationBuilder>().inSet() with singleton { SecurityReport() }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { CachingHeadersFeature() }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { CompressionFeature() }
|
||||
bind<ApplicationBuilder>().inSet() with singleton { CallLoggingFeature() }
|
||||
|
||||
bind() from setBinding<RoutingBuilder>()
|
||||
bind<RoutingBuilder>().inSet() with singleton { ApiDocRoutes() }
|
||||
bind<RoutingBuilder>().inSet() with singleton { ApiNoteRoutes(instance()) }
|
||||
bind<RoutingBuilder>().inSet() with singleton { ApiTagRoutes(instance()) }
|
||||
bind<RoutingBuilder>().inSet() with singleton { ApiUserRoutes(instance()) }
|
||||
|
||||
// API
|
||||
bind() from singleton { ApiNoteController(instance(), instance()) }
|
||||
bind() from singleton { ApiTagController(instance()) }
|
||||
bind() from singleton {
|
||||
ApiUserController(
|
||||
userRepository = instance(),
|
||||
authJWT = instance(tag = "auth"),
|
||||
refreshJWT = instance(tag = "refresh"),
|
||||
passwordHash = instance()
|
||||
)
|
||||
}
|
||||
|
||||
// web
|
||||
bind<RoutingBuilder>().inSet() with singleton { BaseRoutes(instance()) }
|
||||
bind<RoutingBuilder>().inSet() with singleton { UserRoutes(instance()) }
|
||||
bind<RoutingBuilder>().inSet() with singleton { NoteRoutes(instance()) }
|
||||
bind<RoutingBuilder>().inSet() with singleton { StaticRoutes() }
|
||||
|
||||
bind() from singleton { NoteController(instance(), instance(), instance()) }
|
||||
bind() from singleton { BaseController(instance()) }
|
||||
bind() from singleton {
|
||||
UserController(
|
||||
userRepository = instance(),
|
||||
authJWT = instance(tag = "auth"),
|
||||
templates = instance(),
|
||||
passwordHash = instance()
|
||||
)
|
||||
}
|
||||
|
||||
bind<TemplateProvider>() with singleton { ResourceTemplateProvider() }
|
||||
bind<Templates>() with singleton { templatesFactory(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() }
|
||||
|
||||
bind() from singleton { Markdown() }
|
||||
}
|
||||
@ -2,10 +2,11 @@ package be.vandewalleh
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import be.vandewalleh.extensions.RoutingBuilder
|
||||
import io.ktor.application.*
|
||||
import io.ktor.routing.*
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.application.log
|
||||
import io.ktor.routing.routing
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.netty.*
|
||||
import io.ktor.server.netty.Netty
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.description
|
||||
import org.kodein.di.instance
|
||||
@ -47,6 +48,10 @@ fun Application.module(di: DI) {
|
||||
it.builder(this)
|
||||
}
|
||||
|
||||
routing {
|
||||
trace { application.log.trace(it.buildText()) }
|
||||
}
|
||||
|
||||
val routingBuilders: Set<RoutingBuilder> by di.instance()
|
||||
routingBuilders.forEach {
|
||||
routing(it.builder)
|
||||
54
src/auth/AuthenticationModule.kt
Normal file
54
src/auth/AuthenticationModule.kt
Normal file
@ -0,0 +1,54 @@
|
||||
package be.vandewalleh.auth
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.auth.jwt.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.auth.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.util.pipeline.*
|
||||
|
||||
class AuthenticationModule(authJwt: SimpleJWT) : ApplicationBuilder({
|
||||
install(Authentication) {
|
||||
jwt {
|
||||
verifier(authJwt.verifier)
|
||||
authHeader { call ->
|
||||
val token = call.request.header(HttpHeaders.Authorization)
|
||||
?: call.request.cookies["Authorization"]
|
||||
token?.let {
|
||||
try {
|
||||
parseAuthorizationHeader(it)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
validate {
|
||||
UserPrincipal(
|
||||
id = it.payload.getClaim("id").asInt(),
|
||||
username = it.payload.getClaim("username").asString()
|
||||
)
|
||||
}
|
||||
challenge { scheme, realm ->
|
||||
authChallenge(scheme, realm)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
private suspend fun PipelineContext<*, ApplicationCall>.authChallenge(scheme: String, realm: String) {
|
||||
if (call.request.uri.startsWith("/api"))
|
||||
call.respond(
|
||||
UnauthorizedResponse(
|
||||
HttpAuthHeader.Parameterized(
|
||||
scheme,
|
||||
mapOf(HttpAuthHeader.Parameters.Realm to realm)
|
||||
)
|
||||
)
|
||||
)
|
||||
else {
|
||||
call.respondRedirect("/login")
|
||||
}
|
||||
}
|
||||
@ -11,8 +11,9 @@ class SimpleJWT(secret: String, validity: Long, unit: TimeUnit) {
|
||||
private val algorithm = Algorithm.HMAC256(secret)
|
||||
|
||||
val verifier: JWTVerifier = JWT.require(algorithm).build()
|
||||
fun sign(id: Int): String = JWT.create()
|
||||
fun sign(id: Int, username: String): String = JWT.create()
|
||||
.withClaim("id", id)
|
||||
.withClaim("username", username)
|
||||
.withExpiresAt(getExpiration())
|
||||
.sign(algorithm)
|
||||
|
||||
10
src/auth/UserPrincipal.kt
Normal file
10
src/auth/UserPrincipal.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package be.vandewalleh.auth
|
||||
|
||||
import io.ktor.auth.*
|
||||
|
||||
/**
|
||||
* Represents a simple user's principal identified by it's [id] and [username]
|
||||
* @property id
|
||||
* @property username
|
||||
*/
|
||||
data class UserPrincipal(val id: Int, val username: String) : Principal
|
||||
78
src/controllers/api/ApiNoteController.kt
Normal file
78
src/controllers/api/ApiNoteController.kt
Normal file
@ -0,0 +1,78 @@
|
||||
package be.vandewalleh.controllers.api
|
||||
|
||||
import be.vandewalleh.entities.Note
|
||||
import be.vandewalleh.extensions.authenticatedUser
|
||||
import be.vandewalleh.features.ValidationException
|
||||
import be.vandewalleh.markdown.Markdown
|
||||
import be.vandewalleh.markdown.MarkdownDocument
|
||||
import be.vandewalleh.repositories.NoteRepository
|
||||
import be.vandewalleh.utils.Either.Error
|
||||
import be.vandewalleh.utils.Either.Success
|
||||
import io.ktor.application.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import java.util.*
|
||||
|
||||
class ApiNoteController(private val noteRepository: NoteRepository, private val md: Markdown) {
|
||||
|
||||
suspend fun create(call: ApplicationCall) {
|
||||
val userId = call.authenticatedUser().id
|
||||
val txt = call.receiveText()
|
||||
|
||||
val doc: MarkdownDocument
|
||||
|
||||
when (val result = md.renderDocument(txt)) {
|
||||
is Error -> return call.respond(HttpStatusCode.BadRequest, result.error)
|
||||
is Success -> doc = result.value
|
||||
}
|
||||
|
||||
val note = Note {
|
||||
this.title = doc.meta.title
|
||||
this.tags = doc.meta.tags
|
||||
this.markdown = txt
|
||||
this.html = doc.html
|
||||
}
|
||||
|
||||
val createdNote = noteRepository.create(userId, note)
|
||||
call.respond(HttpStatusCode.Created, createdNote)
|
||||
}
|
||||
|
||||
suspend fun getAll(call: ApplicationCall) {
|
||||
val userId = call.authenticatedUser().id
|
||||
val limit = call.parameters["limit"]?.toInt() ?: 20 // FIXME validate
|
||||
val after = call.parameters["after"]?.let { UUID.fromString(it) } // FIXME validate
|
||||
val notes = noteRepository.findAll(userId, limit, after)
|
||||
call.respond(notes)
|
||||
}
|
||||
|
||||
suspend fun getOne(call: ApplicationCall) {
|
||||
val userId = call.authenticatedUser().id
|
||||
val noteUuid = call.noteUuid()
|
||||
|
||||
val response = noteRepository.find(userId, noteUuid) ?: return call.response.status(HttpStatusCode.NotFound)
|
||||
call.respond(response)
|
||||
}
|
||||
|
||||
suspend fun update(call: ApplicationCall) {
|
||||
TODO("Not implemented")
|
||||
}
|
||||
|
||||
suspend fun delete(call: ApplicationCall) {
|
||||
val userId = call.authenticatedUser().id
|
||||
val noteUuid = call.noteUuid()
|
||||
|
||||
val success = noteRepository.delete(userId, noteUuid)
|
||||
if (success) call.response.status(HttpStatusCode.NoContent)
|
||||
else call.response.status(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")
|
||||
}
|
||||
}
|
||||
13
src/controllers/api/ApiTagController.kt
Normal file
13
src/controllers/api/ApiTagController.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package be.vandewalleh.controllers.api
|
||||
|
||||
import be.vandewalleh.extensions.authenticatedUser
|
||||
import be.vandewalleh.repositories.NoteRepository
|
||||
import io.ktor.application.*
|
||||
import io.ktor.response.*
|
||||
|
||||
class ApiTagController(private val noteRepository: NoteRepository) {
|
||||
|
||||
suspend fun getAll(call: ApplicationCall) {
|
||||
call.respond(noteRepository.getTags(call.authenticatedUser().id))
|
||||
}
|
||||
}
|
||||
87
src/controllers/api/ApiUserController.kt
Normal file
87
src/controllers/api/ApiUserController.kt
Normal file
@ -0,0 +1,87 @@
|
||||
package be.vandewalleh.controllers.api
|
||||
|
||||
import be.vandewalleh.auth.SimpleJWT
|
||||
import be.vandewalleh.auth.UsernamePasswordCredential
|
||||
import be.vandewalleh.extensions.authenticatedUser
|
||||
import be.vandewalleh.features.PasswordHash
|
||||
import be.vandewalleh.repositories.UserRepository
|
||||
import be.vandewalleh.validation.receiveValidated
|
||||
import be.vandewalleh.validation.registerValidator
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||
import io.ktor.application.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
|
||||
class ApiUserController(
|
||||
private val authJWT: SimpleJWT,
|
||||
private val refreshJWT: SimpleJWT,
|
||||
private val userRepository: UserRepository,
|
||||
private val passwordHash: PasswordHash
|
||||
) {
|
||||
|
||||
suspend fun create(call: ApplicationCall) {
|
||||
val user = call.receiveValidated(registerValidator)
|
||||
|
||||
if (userRepository.exists(user.username))
|
||||
return call.response.status(HttpStatusCode.Conflict)
|
||||
|
||||
userRepository.create(user.username, user.password)
|
||||
?: return call.response.status(HttpStatusCode.Conflict)
|
||||
|
||||
call.response.status(HttpStatusCode.Created)
|
||||
}
|
||||
|
||||
suspend fun login(call: ApplicationCall) {
|
||||
val credential = call.receive<UsernamePasswordCredential>()
|
||||
|
||||
val user = userRepository.find(credential.username)
|
||||
?: return call.response.status(HttpStatusCode.Unauthorized)
|
||||
|
||||
if (!passwordHash.verify(credential.password, user.password)) {
|
||||
return call.response.status(HttpStatusCode.Unauthorized)
|
||||
}
|
||||
|
||||
val response = DualToken(
|
||||
token = authJWT.sign(user.id, user.username),
|
||||
refreshToken = refreshJWT.sign(user.id, user.username)
|
||||
)
|
||||
return call.respond(response)
|
||||
}
|
||||
|
||||
suspend fun refreshToken(call: ApplicationCall) {
|
||||
val token = call.receive<RefreshToken>().refreshToken
|
||||
|
||||
val id = try {
|
||||
val decodedJWT = refreshJWT.verifier.verify(token)
|
||||
decodedJWT.getClaim("id").asInt()
|
||||
} catch (e: JWTVerificationException) {
|
||||
return call.response.status(HttpStatusCode.Unauthorized)
|
||||
}
|
||||
|
||||
val user = userRepository.find(id) ?: return call.response.status(HttpStatusCode.Unauthorized)
|
||||
|
||||
val response = DualToken(
|
||||
token = authJWT.sign(user.id, user.username),
|
||||
refreshToken = refreshJWT.sign(user.id, user.username)
|
||||
)
|
||||
return call.respond(response)
|
||||
}
|
||||
|
||||
suspend fun delete(call: ApplicationCall) {
|
||||
val userId = call.authenticatedUser().id
|
||||
val success = userRepository.delete(userId)
|
||||
if (success) call.response.status(HttpStatusCode.OK)
|
||||
else call.response.status(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
suspend fun info(call: ApplicationCall) {
|
||||
val id = call.authenticatedUser().id
|
||||
val info = userRepository.find(id)
|
||||
if (info != null) call.respond(mapOf("user" to info))
|
||||
else call.response.status(HttpStatusCode.Unauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
private data class RefreshToken(val refreshToken: String)
|
||||
private data class DualToken(val token: String, val refreshToken: String)
|
||||
13
src/controllers/web/BaseController.kt
Normal file
13
src/controllers/web/BaseController.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package be.vandewalleh.controllers.web
|
||||
|
||||
import be.vandewalleh.extensions.respondKorte
|
||||
import com.soywiz.korte.Templates
|
||||
import io.ktor.application.*
|
||||
|
||||
class BaseController(private val templates: Templates) {
|
||||
|
||||
suspend fun index(call: ApplicationCall) {
|
||||
val template = templates.get("index.html")
|
||||
call.respondKorte(template)
|
||||
}
|
||||
}
|
||||
83
src/controllers/web/NoteController.kt
Normal file
83
src/controllers/web/NoteController.kt
Normal file
@ -0,0 +1,83 @@
|
||||
package be.vandewalleh.controllers.web
|
||||
|
||||
import be.vandewalleh.entities.Note
|
||||
import be.vandewalleh.extensions.authenticatedUser
|
||||
import be.vandewalleh.extensions.respondKorte
|
||||
import be.vandewalleh.markdown.Markdown
|
||||
import be.vandewalleh.repositories.NoteRepository
|
||||
import be.vandewalleh.utils.Either.Error
|
||||
import be.vandewalleh.utils.Either.Success
|
||||
import com.soywiz.korte.Templates
|
||||
import io.ktor.application.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.ContentType.*
|
||||
import io.ktor.http.HttpStatusCode.Companion.BadRequest
|
||||
import io.ktor.http.content.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import java.util.*
|
||||
|
||||
class NoteController(
|
||||
private val noteRepository: NoteRepository,
|
||||
private val templates: Templates,
|
||||
private val md: Markdown
|
||||
) {
|
||||
|
||||
suspend fun renderList(call: ApplicationCall) {
|
||||
val userId = call.authenticatedUser().id
|
||||
val notes = noteRepository.findAll(userId)
|
||||
val template = templates.get("list.html")
|
||||
call.respondKorte(template, "notes" to notes)
|
||||
}
|
||||
|
||||
suspend fun renderOne(call: ApplicationCall) {
|
||||
val uuidParam = call.parameters["uuid"]
|
||||
val uuid = UUID.fromString(uuidParam)
|
||||
val note = noteRepository.find(userId = 1, noteUuid = uuid)
|
||||
val template = templates.get("_uuid.html")
|
||||
call.respondKorte(template, "note" to note)
|
||||
}
|
||||
|
||||
suspend fun new(call: ApplicationCall) {
|
||||
val template = templates.get("new.html")
|
||||
|
||||
if (call.request.httpMethod == HttpMethod.Get ||
|
||||
!call.request.contentType().withoutParameters().match(MultiPart.FormData)
|
||||
) {
|
||||
call.respondKorte(template)
|
||||
return
|
||||
}
|
||||
|
||||
val multipart = call.receiveMultipart()
|
||||
val part = multipart.readPart()
|
||||
|
||||
if (part == null || part !is PartData.FormItem || part.name != "markdown") {
|
||||
call.respondKorte(templates.get("error.html"), "error" to "null", statusCode = BadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
val textAreaValue = part.value
|
||||
|
||||
val result = md.renderDocument(textAreaValue)
|
||||
|
||||
val doc = when (result) {
|
||||
is Error -> return call.respondKorte(
|
||||
template,
|
||||
"error" to result.error.msg,
|
||||
statusCode = BadRequest
|
||||
)
|
||||
is Success -> result.value
|
||||
}
|
||||
|
||||
val note = Note {
|
||||
this.title = doc.meta.title
|
||||
this.tags = doc.meta.tags
|
||||
this.markdown = textAreaValue
|
||||
this.html = doc.html
|
||||
}
|
||||
|
||||
noteRepository.create(call.authenticatedUser().id, note)
|
||||
|
||||
call.respondRedirect("/notes")
|
||||
}
|
||||
}
|
||||
108
src/controllers/web/UserController.kt
Normal file
108
src/controllers/web/UserController.kt
Normal file
@ -0,0 +1,108 @@
|
||||
package be.vandewalleh.controllers.web
|
||||
|
||||
import be.vandewalleh.auth.SimpleJWT
|
||||
import be.vandewalleh.entities.User
|
||||
import be.vandewalleh.extensions.respondKorte
|
||||
import be.vandewalleh.features.PasswordHash
|
||||
import be.vandewalleh.repositories.UserRepository
|
||||
import be.vandewalleh.validation.registerValidator
|
||||
import com.soywiz.korte.Templates
|
||||
import io.ktor.application.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.HttpStatusCode.Companion.Unauthorized
|
||||
import io.ktor.http.content.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
|
||||
class UserController(
|
||||
private val userRepository: UserRepository,
|
||||
private val templates: Templates,
|
||||
private val authJWT: SimpleJWT,
|
||||
private val passwordHash: PasswordHash
|
||||
) {
|
||||
|
||||
suspend fun login(call: ApplicationCall) {
|
||||
val template = templates.get("login.html")
|
||||
|
||||
if (call.request.httpMethod == HttpMethod.Get) {
|
||||
call.respondKorte(template)
|
||||
return
|
||||
}
|
||||
|
||||
val parts = call.receiveMultipart().readAllParts()
|
||||
|
||||
val username = (parts.find { it.name == "username" } as PartData.FormItem).value
|
||||
val password = (parts.find { it.name == "password" } as PartData.FormItem).value
|
||||
|
||||
val user = userRepository.find(username)
|
||||
|
||||
if (user == null) {
|
||||
call.respondKorte(
|
||||
template,
|
||||
"error" to "Invalid credentials",
|
||||
statusCode = Unauthorized
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val verify = passwordHash.verify(password, user.password)
|
||||
|
||||
if (!verify) {
|
||||
call.respondKorte(
|
||||
template,
|
||||
"error" to "Invalid credentials",
|
||||
statusCode = Unauthorized
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val token = authJWT.sign(user.id, user.username)
|
||||
|
||||
call.response.cookies.append("Authorization", "Bearer $token", path = "/")
|
||||
call.respondRedirect("/notes")
|
||||
}
|
||||
|
||||
suspend fun register(call: ApplicationCall) {
|
||||
val template = templates.get("register.html")
|
||||
|
||||
if (call.request.httpMethod == HttpMethod.Get) {
|
||||
call.respondKorte(template)
|
||||
return
|
||||
}
|
||||
|
||||
val parts = call.receiveMultipart().readAllParts()
|
||||
|
||||
val username = (parts.find { it.name == "username" } as PartData.FormItem).value
|
||||
val password = (parts.find { it.name == "password" } as PartData.FormItem).value
|
||||
|
||||
val validation = registerValidator.validate(
|
||||
User {
|
||||
this.username = username
|
||||
this.password = password
|
||||
}
|
||||
)
|
||||
|
||||
if (!validation.isValid) {
|
||||
call.respondKorte(template, "error" to validation.details().map { it.defaultMessage })
|
||||
return
|
||||
}
|
||||
|
||||
if (userRepository.exists(username)) {
|
||||
call.respondKorte(template, "error" to "Please choose another username")
|
||||
return
|
||||
}
|
||||
|
||||
if (userRepository.create(username, password) == null) {
|
||||
// still need a check, for race conditions
|
||||
call.respondKorte(template, "error" to "Please choose another username")
|
||||
return
|
||||
}
|
||||
|
||||
call.respondRedirect("/login")
|
||||
}
|
||||
|
||||
suspend fun logout(call: ApplicationCall) {
|
||||
call.response.cookies.appendExpired("Authorization", path = "/")
|
||||
call.respondRedirect("/")
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,8 @@ interface Note : Entity<Note> {
|
||||
|
||||
var uuid: UUID
|
||||
var title: String
|
||||
var content: String
|
||||
var markdown: String
|
||||
var html: String
|
||||
var updatedAt: LocalDateTime
|
||||
|
||||
@get:JsonIgnore
|
||||
37
src/extensions/ApplicationCallExtensions.kt
Normal file
37
src/extensions/ApplicationCallExtensions.kt
Normal file
@ -0,0 +1,37 @@
|
||||
package be.vandewalleh.extensions
|
||||
|
||||
import be.vandewalleh.auth.UserPrincipal
|
||||
import com.soywiz.korte.Template
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.ContentType.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
|
||||
/**
|
||||
* @return the userId for the currently authenticated user
|
||||
*/
|
||||
fun ApplicationCall.authenticatedUser() = principal<UserPrincipal>()!!
|
||||
|
||||
/**
|
||||
* @return the userId for the currently authenticated user or null
|
||||
*/
|
||||
fun ApplicationCall.authenticatedUserOrNull() = principal<UserPrincipal>()
|
||||
|
||||
suspend fun ApplicationCall.respondKorte(
|
||||
template: Template,
|
||||
vararg args: Pair<String, Any?>,
|
||||
statusCode: HttpStatusCode = HttpStatusCode.OK
|
||||
) {
|
||||
val uri = request.path().trimEnd('/')
|
||||
respondText(Text.Html.withCharset(Charsets.UTF_8), statusCode) {
|
||||
template(
|
||||
mapOf(
|
||||
*args,
|
||||
"url" to uri,
|
||||
"user" to authenticatedUserOrNull()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,12 @@
|
||||
package be.vandewalleh.extensions
|
||||
|
||||
import io.ktor.application.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.routing.*
|
||||
|
||||
abstract class RoutingBuilder(val builder: Routing.() -> Unit)
|
||||
|
||||
abstract class ApplicationBuilder(val builder: Application.() -> Unit)
|
||||
|
||||
val ContentType.Text.Markdown: ContentType
|
||||
get() = ContentType("text", "markdown")
|
||||
10
src/factories/TemplateProviderFactory.kt
Normal file
10
src/factories/TemplateProviderFactory.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package be.vandewalleh.factories
|
||||
|
||||
import com.soywiz.korte.TemplateProvider
|
||||
|
||||
class ResourceTemplateProvider : TemplateProvider {
|
||||
override suspend fun get(template: String): String? {
|
||||
val resource = "/templates/$template"
|
||||
return this.javaClass.getResource(resource)?.readText()
|
||||
}
|
||||
}
|
||||
28
src/factories/TemplatesFactory.kt
Normal file
28
src/factories/TemplatesFactory.kt
Normal file
@ -0,0 +1,28 @@
|
||||
package be.vandewalleh.factories
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.soywiz.korte.TeFunction
|
||||
import com.soywiz.korte.TemplateConfig
|
||||
import com.soywiz.korte.TemplateProvider
|
||||
import com.soywiz.korte.Templates
|
||||
|
||||
fun getResourceAsText(path: String): String {
|
||||
return object {}.javaClass.getResource(path).readText()
|
||||
}
|
||||
|
||||
fun templatesFactory(templateProvider: TemplateProvider): Templates {
|
||||
|
||||
val manifest = getResourceAsText("/css-manifest.json")
|
||||
|
||||
val json = ObjectMapper().registerModule(KotlinModule()).readValue<Map<String, String>>(manifest)
|
||||
|
||||
val fn = TeFunction("styles") { args ->
|
||||
val path = args.firstOrNull()
|
||||
json[path]?.let { "/$it" }
|
||||
}
|
||||
|
||||
val config = TemplateConfig(extraFunctions = listOf(fn))
|
||||
return Templates(templateProvider, config = config, cache = true)
|
||||
}
|
||||
23
src/features/CachingHeadersFeature.kt
Normal file
23
src/features/CachingHeadersFeature.kt
Normal file
@ -0,0 +1,23 @@
|
||||
package be.vandewalleh.features
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import io.ktor.application.*
|
||||
import io.ktor.features.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.content.*
|
||||
|
||||
// FIXME: replace with custom features, since immutable is not present
|
||||
class CachingHeadersFeature : ApplicationBuilder({
|
||||
install(CachingHeaders) {
|
||||
options { outgoingContent ->
|
||||
when (outgoingContent.contentType?.withoutParameters()) {
|
||||
ContentType.Text.CSS -> CachingOptions(
|
||||
CacheControl.MaxAge(
|
||||
maxAgeSeconds = 31557600
|
||||
)
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
12
src/features/CallLoggingFeature.kt
Normal file
12
src/features/CallLoggingFeature.kt
Normal file
@ -0,0 +1,12 @@
|
||||
package be.vandewalleh.features
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import io.ktor.application.*
|
||||
import io.ktor.features.*
|
||||
import org.slf4j.event.Level
|
||||
|
||||
class CallLoggingFeature : ApplicationBuilder({
|
||||
install(CallLogging) {
|
||||
this.level = Level.INFO
|
||||
}
|
||||
})
|
||||
11
src/features/CompressionFeature.kt
Normal file
11
src/features/CompressionFeature.kt
Normal file
@ -0,0 +1,11 @@
|
||||
package be.vandewalleh.features
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import io.ktor.application.*
|
||||
import io.ktor.features.*
|
||||
|
||||
class CompressionFeature : ApplicationBuilder({
|
||||
install(Compression) {
|
||||
default()
|
||||
}
|
||||
})
|
||||
@ -1,18 +1,26 @@
|
||||
package be.vandewalleh.features
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import be.vandewalleh.extensions.respondKorte
|
||||
import com.soywiz.korte.Templates
|
||||
import io.ktor.application.*
|
||||
import io.ktor.features.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.utils.io.errors.*
|
||||
import java.sql.SQLTransientConnectionException
|
||||
|
||||
class ErrorHandler : ApplicationBuilder({
|
||||
class ErrorHandler(templates: Templates) : ApplicationBuilder({
|
||||
install(StatusPages) {
|
||||
|
||||
jacksonErrors()
|
||||
|
||||
status(HttpStatusCode.NotFound, HttpStatusCode.InternalServerError) {
|
||||
if (call.request.path().startsWith("/api")) return@status
|
||||
call.respondKorte(templates.get("error.html"), "status" to it.value)
|
||||
}
|
||||
|
||||
exception<IOException> {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
}
|
||||
111
src/features/SecurityReport.kt
Normal file
111
src/features/SecurityReport.kt
Normal file
@ -0,0 +1,111 @@
|
||||
package be.vandewalleh.features
|
||||
|
||||
import be.vandewalleh.extensions.ApplicationBuilder
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import io.ktor.application.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.content.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import io.ktor.util.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
class SecurityReport : ApplicationBuilder({
|
||||
|
||||
val logger = LoggerFactory.getLogger(SecurityReport::class.java)
|
||||
|
||||
routing {
|
||||
post("/csp-reports") {
|
||||
val txt = call.receiveText()
|
||||
val json = ObjectMapper().registerModule(KotlinModule()).readValue<JsonNode>(txt)
|
||||
logger.info(json.toPrettyString())
|
||||
call.response.status(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
|
||||
install(SecurityHeaders) {
|
||||
reportOnly = false
|
||||
reportUri = "/csp-reports"
|
||||
cspValue = "default-src 'self'"
|
||||
}
|
||||
})
|
||||
|
||||
class SecurityHeaders(configuration: Configuration) {
|
||||
private val logger = LoggerFactory.getLogger(key.name)
|
||||
private val headers = buildHeaders(configuration)
|
||||
|
||||
class Configuration {
|
||||
var reportOnly = true
|
||||
var reportUri = "/csp-reports"
|
||||
|
||||
var csp = true
|
||||
var cspValue = "default-src 'self'"
|
||||
|
||||
var noSniff = true
|
||||
var referrerPolicy = true
|
||||
var xssFilter = true
|
||||
var frameguard = true
|
||||
var featurePolicy = false
|
||||
}
|
||||
|
||||
companion object Feature :
|
||||
ApplicationFeature<ApplicationCallPipeline, SecurityHeaders.Configuration, SecurityHeaders> {
|
||||
override val key = AttributeKey<SecurityHeaders>("SecurityHeaders")
|
||||
|
||||
override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): SecurityHeaders {
|
||||
|
||||
val configuration = SecurityHeaders.Configuration().apply(configure)
|
||||
|
||||
val feature = SecurityHeaders(configuration)
|
||||
|
||||
pipeline.sendPipeline.intercept(ApplicationSendPipeline.After) { subject ->
|
||||
if (subject !is OutgoingContent) return@intercept
|
||||
val contentType = subject.contentType ?: return@intercept
|
||||
if (!ContentType.Text.Html.match(contentType.withoutParameters())) return@intercept
|
||||
if (call.request.path().startsWith("/api")) return@intercept
|
||||
feature.process(call, contentType)
|
||||
}
|
||||
|
||||
return feature
|
||||
}
|
||||
}
|
||||
|
||||
fun process(call: ApplicationCall, contentType: ContentType) {
|
||||
headers.forEach { (name, value) ->
|
||||
call.response.headers.append(name, value)
|
||||
}
|
||||
|
||||
logger.debug("Added security headers")
|
||||
}
|
||||
|
||||
private fun buildHeaders(cfg: Configuration): HashMap<String, String> {
|
||||
val headers = HashMap<String, String>()
|
||||
|
||||
if (cfg.noSniff) headers["X-Content-Type-Options"] = "nosniff"
|
||||
if (cfg.referrerPolicy) headers["Referrer-Policy"] = "no-referrer-when-downgrade"
|
||||
if (cfg.xssFilter) headers["X-XSS-Protection"] = "1; mode=block"
|
||||
if (cfg.frameguard) headers["X-Frame-Options"] = "DENY"
|
||||
|
||||
if (cfg.csp) {
|
||||
val cspValue = if (cfg.reportOnly) cfg.cspValue + "; report-uri ${cfg.reportUri}" else cfg.cspValue
|
||||
val cspKey = if (cfg.reportOnly) "Content-Security-Policy-Report-Only" else "Content-Security-Policy"
|
||||
headers[cspKey] = cspValue
|
||||
}
|
||||
|
||||
if (cfg.featurePolicy) {
|
||||
|
||||
// really ?? https://github.com/w3c/webappsec-feature-policy/issues/189
|
||||
headers["Feature-Policy"] =
|
||||
"accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; battery 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; legacy-image-formats 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; oversized-images 'none'; payment 'none'; picture-in-picture 'none'; publickey-credentials-get 'none'; sync-xhr 'none'; unoptimized-images 'none'; unsized-media 'none'; usb 'none'; wake-lock 'none'; xr-spatial-tracking 'none'"
|
||||
}
|
||||
|
||||
// TODO: Strict-Transport-Security: "max-age=31536000; includeSubDomains"
|
||||
|
||||
return headers
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user