Compare commits
2 Commits
b015f3a97e
...
31f538c7f5
| Author | SHA1 | Date | |
|---|---|---|---|
| 31f538c7f5 | |||
| c7cf71441f |
3
.gitignore
vendored
3
.gitignore
vendored
@ -133,3 +133,6 @@ app/src/main/resources/static/styles*
|
||||
|
||||
# lucene index
|
||||
.lucene/
|
||||
|
||||
# python
|
||||
__pycache__
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package be.simplenotes.app
|
||||
|
||||
import be.simplenotes.app.api.ApiNoteController
|
||||
import be.simplenotes.app.api.ApiUserController
|
||||
import be.simplenotes.app.controllers.BaseController
|
||||
import be.simplenotes.app.controllers.NoteController
|
||||
import be.simplenotes.app.controllers.SettingsController
|
||||
@ -7,6 +9,7 @@ import be.simplenotes.app.controllers.UserController
|
||||
import be.simplenotes.app.filters.AuthFilter
|
||||
import be.simplenotes.app.filters.AuthType
|
||||
import be.simplenotes.app.filters.ErrorFilter
|
||||
import be.simplenotes.app.filters.JwtSource
|
||||
import be.simplenotes.app.routes.Router
|
||||
import be.simplenotes.app.utils.StaticFileResolver
|
||||
import be.simplenotes.app.utils.StaticFileResolverImpl
|
||||
@ -18,12 +21,22 @@ import be.simplenotes.persistance.persistanceModule
|
||||
import be.simplenotes.search.searchModule
|
||||
import be.simplenotes.shared.config.DataSourceConfig
|
||||
import be.simplenotes.shared.config.JwtConfig
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import org.http4k.core.RequestContexts
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.core.qualifier.qualifier
|
||||
import org.koin.dsl.module
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
import be.simplenotes.shared.config.ServerConfig as SimpleNotesServeConfig
|
||||
|
||||
fun main() {
|
||||
@ -38,6 +51,7 @@ fun main() {
|
||||
noteModule,
|
||||
settingsModule,
|
||||
searchModule,
|
||||
apiModule,
|
||||
)
|
||||
}.koin
|
||||
|
||||
@ -68,9 +82,12 @@ val serverModule = module {
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
requiredAuth = get(AuthType.Required.qualifier),
|
||||
optionalAuth = get(AuthType.Optional.qualifier),
|
||||
errorFilter = get(named("ErrorFilter")),
|
||||
apiAuth = get(named("apiAuthFilter")),
|
||||
get()
|
||||
)()
|
||||
}
|
||||
@ -107,3 +124,57 @@ val configModule = module {
|
||||
single { Config.jwtConfig }
|
||||
single { Config.serverConfig }
|
||||
}
|
||||
|
||||
val apiModule = module {
|
||||
single { ApiUserController(get(), get()) }
|
||||
single { ApiNoteController(get(), get()) }
|
||||
single {
|
||||
Json {
|
||||
prettyPrint = true
|
||||
serializersModule = get()
|
||||
}
|
||||
}
|
||||
single {
|
||||
SerializersModule {
|
||||
contextual(LocalDateTime::class, LocalDateTimeSerializer)
|
||||
contextual(UUID::class, UuidSerializer)
|
||||
}
|
||||
}
|
||||
single(named("apiAuthFilter")) {
|
||||
AuthFilter(
|
||||
extractor = get(),
|
||||
authType = AuthType.Required,
|
||||
ctx = get(),
|
||||
source = JwtSource.Header,
|
||||
redirect = false
|
||||
)()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: LocalDateTime) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): LocalDateTime {
|
||||
TODO("Not implemented, isn't needed")
|
||||
}
|
||||
}
|
||||
|
||||
internal object UuidSerializer : KSerializer<UUID> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: UUID) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): UUID {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
77
app/src/main/kotlin/api/ApiNoteController.kt
Normal file
77
app/src/main/kotlin/api/ApiNoteController.kt
Normal file
@ -0,0 +1,77 @@
|
||||
package be.simplenotes.app.api
|
||||
|
||||
import be.simplenotes.app.extensions.json
|
||||
import be.simplenotes.app.utils.parseSearchTerms
|
||||
import be.simplenotes.domain.model.PersistedNote
|
||||
import be.simplenotes.domain.model.PersistedNoteMetadata
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.usecases.NoteService
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status.Companion.BAD_REQUEST
|
||||
import org.http4k.core.Status.Companion.NOT_FOUND
|
||||
import org.http4k.core.Status.Companion.OK
|
||||
import org.http4k.routing.path
|
||||
import java.util.*
|
||||
|
||||
class ApiNoteController(private val noteService: NoteService, private val json: Json) {
|
||||
|
||||
fun createNote(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
|
||||
return noteService.create(jwtPayload.userId, content).fold(
|
||||
{
|
||||
Response(BAD_REQUEST)
|
||||
},
|
||||
{
|
||||
Response(OK).json(json.encodeToString(UuidContent.serializer(), UuidContent(it.uuid)))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun notes(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes
|
||||
val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
|
||||
return Response(OK).json(json)
|
||||
}
|
||||
|
||||
fun note(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val uuid = request.path("uuid")!!
|
||||
|
||||
return noteService.find(jwtPayload.userId, UUID.fromString(uuid))
|
||||
?.let { Response(OK).json(json.encodeToString(PersistedNote.serializer(), it)) }
|
||||
?: Response(NOT_FOUND)
|
||||
}
|
||||
|
||||
fun update(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val uuid = UUID.fromString(request.path("uuid")!!)
|
||||
val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
|
||||
return noteService.update(jwtPayload.userId, uuid, content).fold({
|
||||
Response(BAD_REQUEST)
|
||||
}, {
|
||||
if (it == null) Response(NOT_FOUND)
|
||||
else Response(OK)
|
||||
})
|
||||
}
|
||||
|
||||
fun search(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val query = json.decodeFromString(SearchContent.serializer(), request.bodyString()).query
|
||||
val terms = parseSearchTerms(query)
|
||||
val notes = noteService.search(jwtPayload.userId, terms)
|
||||
val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
|
||||
return Response(OK).json(json)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class NoteContent(val content: String)
|
||||
|
||||
@Serializable
|
||||
data class UuidContent(@Contextual val uuid: UUID)
|
||||
|
||||
@Serializable
|
||||
data class SearchContent(@Contextual val query: String)
|
||||
26
app/src/main/kotlin/api/ApiUserController.kt
Normal file
26
app/src/main/kotlin/api/ApiUserController.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package be.simplenotes.app.api
|
||||
|
||||
import be.simplenotes.app.extensions.json
|
||||
import be.simplenotes.domain.usecases.UserService
|
||||
import be.simplenotes.domain.usecases.users.login.LoginForm
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status
|
||||
|
||||
class ApiUserController(private val userService: UserService, private val json: Json) {
|
||||
|
||||
fun login(request: Request): Response {
|
||||
val form = json.decodeFromString(LoginForm.serializer(), request.bodyString())
|
||||
val result = userService.login(form)
|
||||
return result.fold({
|
||||
Response(Status.BAD_REQUEST)
|
||||
}, {
|
||||
Response(Status.OK).json(json.encodeToString(Token.serializer(), Token(it)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Token(val token: String)
|
||||
@ -9,6 +9,8 @@ fun Response.html(html: String) = body(html)
|
||||
.header("Content-Type", "text/html; charset=utf-8")
|
||||
.header("Cache-Control", "no-cache")
|
||||
|
||||
fun Response.json(json: String) = body(json).header("Content-Type", "application/json")
|
||||
|
||||
fun Response.Companion.redirect(url: String, permanent: Boolean = false) =
|
||||
Response(if (permanent) MOVED_PERMANENTLY else FOUND).header("Location", url)
|
||||
|
||||
|
||||
@ -3,10 +3,8 @@ package be.simplenotes.app.filters
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.security.JwtPayloadExtractor
|
||||
import org.http4k.core.Filter
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.RequestContexts
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.Status.Companion.UNAUTHORIZED
|
||||
import org.http4k.core.cookie.cookie
|
||||
|
||||
enum class AuthType {
|
||||
@ -18,17 +16,26 @@ private const val authKey = "auth"
|
||||
class AuthFilter(
|
||||
private val extractor: JwtPayloadExtractor,
|
||||
private val authType: AuthType,
|
||||
private val ctx: RequestContexts
|
||||
private val ctx: RequestContexts,
|
||||
private val source: JwtSource = JwtSource.Cookie,
|
||||
private val redirect: Boolean = true,
|
||||
) {
|
||||
operator fun invoke() = Filter { next ->
|
||||
{
|
||||
val jwtPayload = it.bearerToken()?.let { token -> extractor(token) }
|
||||
val token = when (source) {
|
||||
JwtSource.Header -> it.bearerTokenHeader()
|
||||
JwtSource.Cookie -> it.bearerTokenCookie()
|
||||
}
|
||||
val jwtPayload = token?.let { token -> extractor(token) }
|
||||
when {
|
||||
jwtPayload != null -> {
|
||||
ctx[it][authKey] = jwtPayload
|
||||
next(it)
|
||||
}
|
||||
authType == AuthType.Required -> Response.redirect("/login")
|
||||
authType == AuthType.Required -> {
|
||||
if (redirect) Response.redirect("/login")
|
||||
else Response(UNAUTHORIZED)
|
||||
}
|
||||
else -> next(it)
|
||||
}
|
||||
}
|
||||
@ -37,6 +44,17 @@ class AuthFilter(
|
||||
|
||||
fun Request.jwtPayload(ctx: RequestContexts): JwtPayload? = ctx[this][authKey]
|
||||
|
||||
private fun Request.bearerToken(): String? = cookie("Bearer")
|
||||
enum class JwtSource {
|
||||
Header, Cookie
|
||||
}
|
||||
|
||||
private fun Request.bearerTokenCookie(): String? = cookie("Bearer")
|
||||
?.value
|
||||
?.trim()
|
||||
|
||||
private fun Request.bearerTokenHeader(): String? =
|
||||
header("Authorization")
|
||||
?.trim()
|
||||
?.takeIf { it.startsWith("Bearer") }
|
||||
?.substringAfter("Bearer")
|
||||
?.trim()
|
||||
|
||||
@ -6,14 +6,9 @@ import org.http4k.core.Method
|
||||
import org.http4k.core.Request
|
||||
|
||||
object ImmutableFilter {
|
||||
operator fun invoke(): Filter {
|
||||
return Filter { next: HttpHandler ->
|
||||
{ request: Request ->
|
||||
val response = next(request)
|
||||
if (request.method == Method.GET)
|
||||
response.header("Cache-Control", "public, max-age=31536000, immutable")
|
||||
else response
|
||||
}
|
||||
operator fun invoke() = Filter { next: HttpHandler ->
|
||||
{ request: Request ->
|
||||
next(request).header("Cache-Control", "public, max-age=31536000, immutable")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package be.simplenotes.app.routes
|
||||
|
||||
import be.simplenotes.app.api.ApiNoteController
|
||||
import be.simplenotes.app.api.ApiUserController
|
||||
import be.simplenotes.app.controllers.BaseController
|
||||
import be.simplenotes.app.controllers.NoteController
|
||||
import be.simplenotes.app.controllers.SettingsController
|
||||
@ -9,8 +11,7 @@ import be.simplenotes.app.filters.SecurityFilter
|
||||
import be.simplenotes.app.filters.jwtPayload
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.Method.GET
|
||||
import org.http4k.core.Method.POST
|
||||
import org.http4k.core.Method.*
|
||||
import org.http4k.filter.ResponseFilters
|
||||
import org.http4k.filter.ServerFilters.InitialiseRequestContext
|
||||
import org.http4k.routing.*
|
||||
@ -20,9 +21,12 @@ class Router(
|
||||
private val userController: UserController,
|
||||
private val noteController: NoteController,
|
||||
private val settingsController: SettingsController,
|
||||
private val apiUserController: ApiUserController,
|
||||
private val apiNoteController: ApiNoteController,
|
||||
private val requiredAuth: Filter,
|
||||
private val optionalAuth: Filter,
|
||||
private val errorFilter: Filter,
|
||||
private val apiAuth: Filter,
|
||||
private val contexts: RequestContexts,
|
||||
) {
|
||||
operator fun invoke(): RoutingHttpHandler {
|
||||
@ -61,10 +65,24 @@ class Router(
|
||||
"/notes/deleted/{uuid}" bind POST protected noteController::deleted,
|
||||
)
|
||||
|
||||
val apiRoutes = routes(
|
||||
"/api/login" bind POST to apiUserController::login,
|
||||
)
|
||||
|
||||
val protectedApiRoutes = routes(
|
||||
"/api/notes" bind GET protected apiNoteController::notes,
|
||||
"/api/notes" bind POST protected apiNoteController::createNote,
|
||||
"/api/notes/search" bind POST protected apiNoteController::search,
|
||||
"/api/notes/{uuid}" bind GET protected apiNoteController::note,
|
||||
"/api/notes/{uuid}" bind PUT protected apiNoteController::update,
|
||||
)
|
||||
|
||||
val routes = routes(
|
||||
basicRoutes,
|
||||
optionalAuth.then(publicRoutes),
|
||||
requiredAuth.then(protectedRoutes),
|
||||
apiAuth.then(protectedApiRoutes),
|
||||
apiRoutes,
|
||||
)
|
||||
|
||||
val globalFilters = errorFilter
|
||||
|
||||
38
cli/Config.py
Normal file
38
cli/Config.py
Normal file
@ -0,0 +1,38 @@
|
||||
import configparser
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import appdirs
|
||||
import click
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
config_dir: str = appdirs.user_config_dir("SimpleNotesCli")
|
||||
Path(config_dir).mkdir(exist_ok=True)
|
||||
self.file = os.path.join(config_dir, "config.ini")
|
||||
self.configparser = configparser.ConfigParser()
|
||||
|
||||
self.token = None
|
||||
self.base_url = None
|
||||
|
||||
def load(self):
|
||||
self.configparser.read(self.file)
|
||||
self.token = self.configparser.get("DEFAULT", "token", fallback=None)
|
||||
self.base_url = self.configparser.get(
|
||||
"DEFAULT", "base_url", fallback="https://simplenotes.be"
|
||||
)
|
||||
|
||||
def save(self):
|
||||
if self.token:
|
||||
self.configparser.set("DEFAULT", "token", self.token)
|
||||
|
||||
if self.base_url:
|
||||
self.configparser.set("DEFAULT", "base_url", self.base_url)
|
||||
|
||||
try:
|
||||
with open(self.file, "w") as f:
|
||||
self.configparser.write(f)
|
||||
except IOError:
|
||||
click.secho("An error occurred while saving config", fg="red", err=True)
|
||||
exit(1)
|
||||
66
cli/SimpleNotesApi.py
Normal file
66
cli/SimpleNotesApi.py
Normal file
@ -0,0 +1,66 @@
|
||||
from typing import List, Optional
|
||||
|
||||
import click
|
||||
import requests
|
||||
from requests.models import Response
|
||||
|
||||
from domain import NoteMetadata
|
||||
|
||||
|
||||
class SimplenotesApi:
|
||||
def __init__(self, base_url: str):
|
||||
self.base_url = base_url
|
||||
self.s = requests.Session()
|
||||
self.s.hooks["response"] = [self.__exit_unauthorized]
|
||||
|
||||
def __url(self, path: str) -> str:
|
||||
return f"{self.base_url}{path}"
|
||||
|
||||
def __exit_unauthorized(self, response: Response, *args, **kwargs):
|
||||
if response.status_code == 401:
|
||||
click.secho("Unauthorized, please login again", fg="red", err=True)
|
||||
exit(1)
|
||||
|
||||
def login(self, username: str, password: str) -> Optional[str]:
|
||||
url = self.__url("/api/login")
|
||||
r = self.s.post(
|
||||
url,
|
||||
json={"username": username, "password": password},
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return r.json()["token"]
|
||||
|
||||
def set_token(self, token: str):
|
||||
self.s.headers.update({"Authorization": f"Bearer {token}"})
|
||||
|
||||
def find_note(self, uuid: str) -> Optional[str]:
|
||||
url = self.__url(f"/api/notes/{uuid}")
|
||||
r = self.s.get(url)
|
||||
if r.status_code == 200:
|
||||
return r.json()["markdown"]
|
||||
|
||||
def list_notes(self) -> List[NoteMetadata]:
|
||||
url = self.__url("/api/notes")
|
||||
r = self.s.get(url)
|
||||
return list(map(lambda x: NoteMetadata(**x), r.json()))
|
||||
|
||||
def search_notes(self, query: str) -> List[NoteMetadata]:
|
||||
url = self.__url("/api/notes/search")
|
||||
r = self.s.post(url, json={"query": query})
|
||||
|
||||
if r.status_code == 200:
|
||||
j = r.json()
|
||||
return list(map(lambda x: NoteMetadata(**x), j))
|
||||
else:
|
||||
return []
|
||||
|
||||
def create_note(self, content: str) -> Optional[str]:
|
||||
url = self.__url("/api/notes/")
|
||||
r = self.s.post(url, json={"content": content})
|
||||
if r.status_code == 200:
|
||||
return r.json()["uuid"]
|
||||
|
||||
def update_note(self, uuid: str, content: str) -> bool:
|
||||
url = self.__url(f"/api/notes/{uuid}")
|
||||
r = self.s.put(url, json={"content": content})
|
||||
return r.status_code == 200
|
||||
112
cli/SimpleNotesCli.py
Executable file
112
cli/SimpleNotesCli.py
Executable file
@ -0,0 +1,112 @@
|
||||
import click
|
||||
from click import Context
|
||||
|
||||
import SimpleNotesApi
|
||||
import utils
|
||||
from Config import Config
|
||||
from jwtutils import is_expired
|
||||
from utils import edit_md
|
||||
|
||||
api: SimpleNotesApi
|
||||
base_url: str
|
||||
conf = Config()
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.pass_context
|
||||
def cli(ctx: Context):
|
||||
global base_url
|
||||
global api
|
||||
|
||||
conf.load()
|
||||
base_url = conf.base_url
|
||||
api = SimpleNotesApi.SimplenotesApi(base_url)
|
||||
|
||||
if ctx.invoked_subcommand == "login" or ctx.invoked_subcommand == "config":
|
||||
return
|
||||
|
||||
token = conf.token
|
||||
if token is None:
|
||||
click.secho("Please login", err=True, fg="red")
|
||||
exit(1)
|
||||
elif is_expired(token):
|
||||
click.secho("Login expired, please login again", err=True, fg="red")
|
||||
exit(1)
|
||||
else:
|
||||
api.set_token(token)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--username", prompt=True)
|
||||
@click.option("--password", prompt=True, hide_input=True)
|
||||
def login(username: str, password: str):
|
||||
token = api.login(username, password)
|
||||
if token:
|
||||
conf.token = token
|
||||
conf.save()
|
||||
click.secho(f"Welcome {username}", fg="green")
|
||||
else:
|
||||
click.echo("Invalid credentials")
|
||||
exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--url", prompt=True)
|
||||
def config(url: str):
|
||||
conf.base_url = url
|
||||
conf.save()
|
||||
|
||||
|
||||
@cli.command(name="list")
|
||||
def list_notes():
|
||||
utils.print_notes(api.list_notes())
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("uuid")
|
||||
def edit(uuid: str):
|
||||
note = api.find_note(uuid)
|
||||
if not note:
|
||||
click.secho("Note not found", err=True, fg="red")
|
||||
exit(1)
|
||||
|
||||
edited = edit_md(note)
|
||||
|
||||
if edited == note:
|
||||
exit(1)
|
||||
|
||||
if not api.update_note(uuid, edited):
|
||||
click.secho("An error occurred", err=True, fg="red")
|
||||
exit(1)
|
||||
else:
|
||||
utils.print_note_url(uuid, "updated", conf.base_url)
|
||||
|
||||
|
||||
@cli.command()
|
||||
def new():
|
||||
placeholder = "---\ntitle: ''\ntags: []\n---\n"
|
||||
md = edit_md(placeholder)
|
||||
if md == placeholder:
|
||||
exit(1)
|
||||
uuid = api.create_note(md)
|
||||
if uuid:
|
||||
utils.print_note_url(uuid, "created", conf.base_url)
|
||||
else:
|
||||
click.secho("An error occurred", err=True, fg="red")
|
||||
exit(1)
|
||||
|
||||
|
||||
@cli.command(name="search")
|
||||
@click.argument("search", nargs=-1, required=True)
|
||||
def search_notes(search: str):
|
||||
query = " ".join(search)
|
||||
notes = api.search_notes(query)
|
||||
|
||||
if not notes:
|
||||
print("No match")
|
||||
else:
|
||||
utils.print_notes(notes)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
12
cli/domain.py
Normal file
12
cli/domain.py
Normal file
@ -0,0 +1,12 @@
|
||||
from typing import List
|
||||
|
||||
|
||||
class NoteMetadata:
|
||||
def __init__(self, uuid: str, title: str, tags: List[str], updatedAt: str):
|
||||
self.uuid = uuid
|
||||
self.title = title
|
||||
self.tags = tags
|
||||
self.updated_at = updatedAt
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"NoteMetadata(uuid={self.uuid}, title={self.title}, tags={self.tags})"
|
||||
10
cli/jwtutils.py
Normal file
10
cli/jwtutils.py
Normal file
@ -0,0 +1,10 @@
|
||||
from datetime import datetime
|
||||
|
||||
import jwt
|
||||
|
||||
|
||||
def is_expired(token: str) -> bool:
|
||||
exp = jwt.decode(token, verify=False)["exp"]
|
||||
dt = datetime.utcfromtimestamp(exp)
|
||||
now = datetime.utcnow()
|
||||
return dt < now
|
||||
6
cli/requirements.txt
Normal file
6
cli/requirements.txt
Normal file
@ -0,0 +1,6 @@
|
||||
requests==2.24.0
|
||||
python-editor==1.0.4
|
||||
click==7.1.2
|
||||
PyJWT==1.7.1
|
||||
appdirs==1.4.4
|
||||
tabulate==0.8.7
|
||||
32
cli/utils.py
Normal file
32
cli/utils.py
Normal file
@ -0,0 +1,32 @@
|
||||
from typing import List
|
||||
|
||||
import editor
|
||||
import click
|
||||
from tabulate import tabulate
|
||||
|
||||
from domain import NoteMetadata
|
||||
|
||||
|
||||
def edit_md(content: str = "") -> str:
|
||||
b = bytes(content, "utf-8")
|
||||
result = editor.edit(contents=b, suffix=".md")
|
||||
return result.decode("utf-8")
|
||||
|
||||
|
||||
def print_notes(notes: List[NoteMetadata]):
|
||||
data = []
|
||||
for n in notes:
|
||||
uuid = click.style(n.uuid, fg="blue")
|
||||
title = click.style(n.title, fg="green", bold=True)
|
||||
tags = ["#" + e for e in n.tags]
|
||||
tags = " ".join(tags)
|
||||
data.append([uuid, title, tags])
|
||||
|
||||
headers = ["UUID", "title", "tags"]
|
||||
click.echo(tabulate(data, headers=headers))
|
||||
|
||||
|
||||
def print_note_url(uuid: str, action: str, base_url: str):
|
||||
url = f"{base_url}/notes/{uuid}"
|
||||
s = click.style(url, fg="green", underline=True)
|
||||
click.echo(f"Note {action}: {s}")
|
||||
@ -5,30 +5,34 @@ import kotlinx.serialization.Serializable
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
@Serializable
|
||||
data class NoteMetadata(
|
||||
val title: String,
|
||||
val tags: List<String>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PersistedNoteMetadata(
|
||||
val title: String,
|
||||
val tags: List<String>,
|
||||
val updatedAt: LocalDateTime,
|
||||
val uuid: UUID,
|
||||
@Contextual val updatedAt: LocalDateTime,
|
||||
@Contextual val uuid: UUID,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Note(
|
||||
val meta: NoteMetadata,
|
||||
val markdown: String,
|
||||
val html: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PersistedNote(
|
||||
val meta: NoteMetadata,
|
||||
val markdown: String,
|
||||
val html: String,
|
||||
val updatedAt: LocalDateTime,
|
||||
val uuid: UUID,
|
||||
@Contextual val updatedAt: LocalDateTime,
|
||||
@Contextual val uuid: UUID,
|
||||
val public: Boolean,
|
||||
)
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import be.simplenotes.domain.usecases.repositories.NoteRepository
|
||||
import be.simplenotes.domain.usecases.repositories.UserRepository
|
||||
import be.simplenotes.domain.usecases.search.NoteSearcher
|
||||
import be.simplenotes.domain.usecases.search.SearchTerms
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.*
|
||||
|
||||
class NoteService(
|
||||
|
||||
@ -2,6 +2,7 @@ package be.simplenotes.domain.usecases.users.login
|
||||
|
||||
import arrow.core.Either
|
||||
import io.konform.validation.ValidationErrors
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
sealed class LoginError
|
||||
object Unregistered : LoginError()
|
||||
@ -10,6 +11,7 @@ class InvalidLoginForm(val validationErrors: ValidationErrors) : LoginError()
|
||||
|
||||
typealias Token = String
|
||||
|
||||
@Serializable
|
||||
data class LoginForm(val username: String?, val password: String?)
|
||||
|
||||
interface LoginUseCase {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user