diff --git a/README.md b/README.md index 39e9ca1..6d24c89 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,9 @@ https://diary-log.fly.dev/ (try with a random email is ok) ## set env var ```sh -DATABASE_URL: db url -JWT_AUDIENCE: audience_xxx -JWT_SECRET: jwt_secret_xxx +DATABASE_URL: postgresql://etc ``` + ## check docker-compose.yaml docker-compose up -d diff --git a/api/Note.fs b/api/Note.fs index 0f6862c..b5aeaca 100644 --- a/api/Note.fs +++ b/api/Note.fs @@ -5,6 +5,7 @@ open Microsoft.AspNetCore.Http open Falco open System.Security.Claims open Npgsql +open System.Text.Json let forbidden = let message = "Access to the resource is forbidden." @@ -175,3 +176,74 @@ let login: HttpHandler = let jwt = createNewUser conn email password Json.Response.ofJson jwt) ctx + +open System.Text.Json + +// find all todo_list in note and return todo_list as json str +let extractTodoList (note: string) = + let jsonDocument = JsonDocument.Parse(note) + let root = jsonDocument.RootElement + + let rec extractTodoItems (element: JsonElement) = + seq { + match element.ValueKind with + | JsonValueKind.Object -> + match element.TryGetProperty("type") with + | true, typeProperty when typeProperty.GetString() = "todo_list" -> + yield element + | _ -> + for property in element.EnumerateObject() do + yield! extractTodoItems property.Value + | JsonValueKind.Array -> + for item in element.EnumerateArray() do + yield! extractTodoItems item + | _ -> () + } + + extractTodoItems root |> Seq.toList + + +let getAllTodoLists conn userId = + let diaries = Diary.ListDiaryByUserID conn userId + diaries + |> List.map (fun diary -> + {| noteId = diary.NoteId; todoList = extractTodoList diary.Note |}) + + +let todoListsHandler : HttpHandler = + fun ctx -> + let conn = ctx.getNpgsql () + let userId = int (ctx.User.FindFirst("user_id").Value) + let todoLists = getAllTodoLists conn userId + // print todolists + printfn "%A" todoLists + // construct a tiptap doc with todoLists and note_id + let tiptapDoc = + {| + ``type`` = "doc" + content = [| + for todoList in todoLists do + yield JsonSerializer.Deserialize(""" + { + "type": "heading", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null, + "level": 3 + }, + "content": [ + { + "type": "text", + "text": """ + todoList.noteId + """ + } + ] + } + """) + yield! todoList.todoList + |] + |} + + printfn "%A" tiptapDoc + + Json.Response.ofJson tiptapDoc ctx diff --git a/api/Program.fs b/api/Program.fs index 0cd4fb6..f61da64 100644 --- a/api/Program.fs +++ b/api/Program.fs @@ -54,13 +54,56 @@ let authenticateRouteMiddleware (app: IApplicationBuilder) = app.Use(middleware) - - +let getOrCreateJwtSecret pgConn jwtAudienceName = + let getExistingSecret () = + try + let secret = JwtSecrets.GetJwtSecret pgConn jwtAudienceName + printfn "Existing JWT Secret found for %s" jwtAudienceName + Some secret + with :? NoResultsException -> None + + let generateRandomKey () = + System.Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(32)) + + let getJwtKey () = + match Util.getEnvVar "JWT_SECRET" with + | null -> + let randomKey = generateRandomKey() + printfn "Warning: JWT_SECRET not set. Using randomly generated key: %s" randomKey + randomKey + | key -> key + + let getAudience () = + match Util.getEnvVar "JWT_AUDIENCE" with + | null -> + let defaultAudience = "http://localhost:5000" + printfn "Warning: JWT_AUDIENCE not set. Using default audience: %s" defaultAudience + defaultAudience + | aud -> aud + + let createNewSecret () = + let jwtSecretParams: JwtSecrets.CreateJwtSecretParams = + { Name = jwtAudienceName + Secret = getJwtKey() + Audience = getAudience() } + let createdSecret = JwtSecrets.CreateJwtSecret pgConn jwtSecretParams + printfn "New JWT Secret created for %s" jwtAudienceName + createdSecret + + match getExistingSecret() with + | Some secret -> secret + | None -> createNewSecret() let authService (services: IServiceCollection) = - let jwtKey = Util.getEnvVar "JWT_SECRET" - let audience = Util.getEnvVar "JWT_AUDIENCE" + let connectionString = Database.Config.connStr + use pgConn = new Npgsql.NpgsqlConnection(connectionString) + pgConn.Open() + + let jwtAudienceName = "logbook" + let jwtSecret = getOrCreateJwtSecret pgConn jwtAudienceName + + pgConn.Close() let _ = services @@ -72,9 +115,9 @@ let authService (services: IServiceCollection) = ValidateIssuer = false, //ValidIssuer = Configuratio["Jwt:Issuer"], ValidateAudience = true, - ValidAudience = audience, + ValidAudience = jwtSecret.Audience, ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(jwtKey)) + IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(jwtSecret.Secret)) )) services @@ -102,7 +145,9 @@ webHost [||] { [ post "/api/login" Note.login get "/api/diary" Note.noteAllPart get "/api/diary/{id}" Note.noteByIdPartDebug - put "/api/diary/{id}" Note.addNotePart ] + put "/api/diary/{id}" Note.addNotePart + get "/api/todo" Note.todoListsHandler + ] use_middleware serveVueFiles } diff --git a/api/Util.fs b/api/Util.fs index a8bd595..fb67359 100644 --- a/api/Util.fs +++ b/api/Util.fs @@ -1,6 +1,3 @@ module Util -let getEnvVar varName = - match System.Environment.GetEnvironmentVariable(varName) with - | null -> failwith (sprintf "%s environment variable not found" varName) - | value -> value +let getEnvVar varName = System.Environment.GetEnvironmentVariable(varName) diff --git a/api/tests/sample_diary_todo_list.json b/api/tests/sample_diary_todo_list.json new file mode 100644 index 0000000..1d6e7c2 --- /dev/null +++ b/api/tests/sample_diary_todo_list.json @@ -0,0 +1,142 @@ +[ + { + "noteId": "20240630", + "todoList": [ + { + "type": "todo_list", + "content": [ + { + "type": "todo_item", + "attrs": { + "done": false, + "textAlign": null, + "lineHeight": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null + }, + "content": [ + { + "type": "text", + "text": "a" + } + ] + } + ] + }, + { + "type": "todo_item", + "attrs": { + "done": false, + "textAlign": null, + "lineHeight": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null + }, + "content": [ + { + "type": "text", + "text": "b" + } + ] + } + ] + }, + { + "type": "todo_item", + "attrs": { + "done": false, + "textAlign": null, + "lineHeight": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null + }, + "content": [ + { + "type": "text", + "text": "c" + } + ] + } + ] + } + ] + } + ] + }, + { + "noteId": "20240830", + "todoList": [ + { + "type": "todo_list", + "content": [ + { + "type": "todo_item", + "attrs": { + "done": true, + "textAlign": null, + "lineHeight": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null + }, + "content": [ + { + "type": "text", + "text": "tedst" + } + ] + } + ] + }, + { + "type": "todo_item", + "attrs": { + "done": false, + "textAlign": null, + "lineHeight": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null + }, + "content": [ + { + "type": "text", + "text": "test" + } + ] + } + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/api/tests/sample_note.json b/api/tests/sample_note.json new file mode 100644 index 0000000..7d970af --- /dev/null +++ b/api/tests/sample_note.json @@ -0,0 +1,110 @@ +{ + "type": "doc", + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null + }, + "content": [ + { + "type": "text", + "text": "test" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null + }, + "content": [ + { + "type": "text", + "text": "test" + } + ] + }, + { + "type": "todo_list", + "content": [ + { + "type": "todo_item", + "attrs": { + "done": true, + "textAlign": null, + "lineHeight": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null + }, + "content": [ + { + "type": "text", + "text": "tedst" + } + ] + } + ] + }, + { + "type": "todo_item", + "attrs": { + "done": false, + "textAlign": null, + "lineHeight": null + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null + }, + "content": [ + { + "type": "text", + "text": "test" + } + ] + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null + } + }, + { + "type": "paragraph", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null + } + }, + { + "type": "paragraph", + "attrs": { + "textAlign": null, + "indent": null, + "lineHeight": null + } + } + ] + } \ No newline at end of file