diff of ca95f9260092899b8ed6d7bf60137ad2df1e5adc

ca95f9260092899b8ed6d7bf60137ad2df1e5adc
diff --git a/aggressive-murja.asd b/aggressive-murja.asd
index 3e1a42a..4703adc 100644
--- a/aggressive-murja.asd
+++ b/aggressive-murja.asd
@@ -42,7 +42,8 @@
 
 		 (:module "routes"
 		  :components
-		  ((:file "login-routes")
+		  ((:file "settings-routes")
+		   (:file "login-routes")
 		   (:file "post-routes")
 		   (:file "media-routes")
 		   (:file "root-routes")))
diff --git a/elm-frontti/src/Ajax_cmds.elm b/elm-frontti/src/Ajax_cmds.elm
index 57b7e00..a697a05 100644
--- a/elm-frontti/src/Ajax_cmds.elm
+++ b/elm-frontti/src/Ajax_cmds.elm
@@ -32,12 +32,16 @@ getPost post_id =
         { url = "/api/posts/post/" ++ (String.fromInt post_id)
         , expect = Http.expectJson PostReceived Article.articleDecoder}
 
-getSettings : Cmd Msg
 getSettings =
     Http.get
         { url = "/api/settings/client-settings"
         , expect = Http.expectJson SettingsReceived Settings.settingsDecoder}
 
+getSettingsAdmin =
+    Http.get
+        { url = "/api/settings/client-settings"
+        , expect = Http.expectJson AdminSettingsReceived Settings.settingsDecoder}        
+
 getTitles =
     Http.get
         { url = "/api/posts/titles"
@@ -113,3 +117,13 @@ generateNewPost =
         , timeout = Nothing
         , tracker = Nothing
         }
+        
+saveSettings settings =
+    Http.request
+        { method = "PUT"
+        , headers = []
+        , url = "/api/settings/client-settings"
+        , body = Http.jsonBody (Settings.encodeSettings settings)
+        , expect = Http.expectWhatever SettingsSaved
+        , timeout = Nothing
+        , tracker = Nothing}
diff --git a/elm-frontti/src/Main.elm b/elm-frontti/src/Main.elm
index 212c705..14caebf 100644
--- a/elm-frontti/src/Main.elm
+++ b/elm-frontti/src/Main.elm
@@ -18,6 +18,7 @@ import User
 import Topbar
 import PostsAdmin
 import PostEditor
+import SettingsEditor
 import Medialist exposing (medialist)
 import Image
 import ImageSelector exposing (imageSelector)
@@ -64,7 +65,7 @@ subscriptions _ = Sub.batch
                   [ tags ReceivedTag
                   , aceStateUpdate AceStateUpdate]
 
-initialModel url key viewstate = Model viewstate Nothing False False [] Nothing LoggedOut key url Nothing Time.utc Nothing []
+initialModel url key viewstate = Model viewstate Nothing False False [] Nothing LoggedOut key url Nothing Time.utc []
     
 viewStatePerUrl : Url.Url -> (ViewState, List (Cmd Msg))
 viewStatePerUrl url =
@@ -106,6 +107,9 @@ viewStatePerUrl url =
                                                                 , loadPostVersion post_id version_id])
                                                                       
         RouteParser.NotFound -> (ShowError ("Couldn't parse url " ++ (Url.toString url)), [Cmd.none])
+        RouteParser.SettingsEditor -> (Loading, [ getSession
+                                                , getSettingsAdmin 
+                                                , getTitles])
     
 init _ url key =
     let (viewstate, cmds) = (viewStatePerUrl url)
@@ -429,6 +433,53 @@ update msg model =
                                                            {settings | article = toggleUnlisted settings.article})
                    model.postEditorSettings}
             , Cmd.none)
+        AdminSettingsReceived result -> 
+            case result of
+                Ok new_settings ->
+                    ({model
+                         | settings = Just new_settings
+                         , view_state = SettingsEditor}, Cmd.none)
+                        
+                Err http_error ->
+                    ( model
+                    , alert ("Error loading settings " ++ Debug.toString http_error))
+        SetTimeFormat tf ->
+            ({ model | settings = Maybe.map (\settings ->
+                                                 { settings | time_format = tf})
+                   model.settings}
+            , Cmd.none)
+        SetBlogTitle title ->
+            ({ model | settings = Maybe.map (\settings ->
+                                                 { settings | blog_title = title})
+                   model.settings}
+            , Cmd.none)
+        SetPageSize pg_size ->
+            case String.toInt pg_size of
+                Just page_size -> 
+                    ({ model | settings = Maybe.map (\settings ->
+                                                         { settings | recent_post_count = page_size})
+                           model.settings}
+                    , Cmd.none)
+                Nothing ->
+                    ( model
+                    , alert "Page size should be a number")
+        SaveSettings ->
+            case model.settings of
+                Just settings -> 
+                    ( model
+                    , saveSettings settings)
+                Nothing ->
+                    ( model
+                    , Cmd.none)
+        SettingsSaved result ->
+            case result of
+                Ok _ ->
+                    ( model
+                    , Cmd.none)
+                        
+                Err http_error ->
+                    ( model
+                    , alert ("Error saving settings " ++ Debug.toString http_error))    
             
 doGoHome_ model other_cmds =
     (model, Cmd.batch (List.append [ getSettings
@@ -517,7 +568,8 @@ view model =
                                                            tag_index = editorSettings.selected_tag in
                                                        PostEditor.postEditor post tag_index model.showImageModal model.loadedImages model.draggingImages editorSettings settings model.zone model.loginState
                                                    Nothing -> [ div [] [ text "No post loaded" ]]
-                                           MediaList -> [ medialist model.loadedImages model.medialist_state ])
+                                           MediaList -> [ medialist model.loadedImages model.medialist_state ]
+                                           SettingsEditor -> [ SettingsEditor.editor settings])
                         , div [id "sidebar"] [ User.loginView model.loginState
                                              , (sidebarHistory model.titles )
                                              , (case model.view_state of
diff --git a/elm-frontti/src/Message.elm b/elm-frontti/src/Message.elm
index 524504e..8eb44b0 100644
--- a/elm-frontti/src/Message.elm
+++ b/elm-frontti/src/Message.elm
@@ -28,6 +28,7 @@ type ViewState
     | PostEditor
     | MediaList                     -- list all the image blobs in db
     | TaggedPostsView (List Article.Article)
+    | SettingsEditor
       
 type alias User =
     { username : String
@@ -71,9 +72,7 @@ type alias Model =
     , url : Url.Url
     , postEditorSettings: Maybe PostEditorSettings
     , zone : Time.Zone
-    , postFromLocalStorage : Maybe Article.Article
-    , titles : List Article.Title
-    }
+    , titles : List Article.Title }
     
 type Msg
   = PageReceived (Result Http.Error P.Page)
@@ -125,14 +124,16 @@ type Msg
   | NewPostGenerated (Result Http.Error Int)
   | ToggleArticleUnlisted
   | ToggleArticleHidden
-  
-
+  | AdminSettingsReceived (Result Http.Error Settings.Settings)
+  | SetTimeFormat String
+  | SetBlogTitle String
+  | SetPageSize String
+  | SaveSettings
+  | SettingsSaved (Result Http.Error ())
 
 -- ports
 port reallySetupAce : String -> Cmd msg
 port addImgToAce : String -> Cmd msg
-port clearPostFromLS : () -> Cmd msg                   
-
 
 -- dumb shit that would deserve its own module
 dangerouslySetInnerHTML: String -> Html.Attribute msg
diff --git a/elm-frontti/src/RouteParser.elm b/elm-frontti/src/RouteParser.elm
index 0a0dbb4..2ea17a7 100644
--- a/elm-frontti/src/RouteParser.elm
+++ b/elm-frontti/src/RouteParser.elm
@@ -12,6 +12,7 @@ type Route
     | PostEditor Int
     | TaggedPosts String
     | PostVersion Int Int
+    | SettingsEditor 
     | Home
     | NotFound
 
@@ -24,6 +25,7 @@ routeParser =
         , map Post (s "blog" </> (s "post" </> int))
         , map PostEditor (s "blog" </> (s "post" </> (s "edit" </> int)))
         , map MediaManager (s "blog" </> (s "mediamanager"))
+        , map SettingsEditor (s "blog" </> (s "settings"))
         , map TaggedPosts (s "blog" </> (s "tags" </> string))
         , map PostAdmin (s "blog" </> (s "postadmin"))]
 
diff --git a/elm-frontti/src/Settings.elm b/elm-frontti/src/Settings.elm
index d3d4f34..900aa19 100644
--- a/elm-frontti/src/Settings.elm
+++ b/elm-frontti/src/Settings.elm
@@ -1,29 +1,22 @@
--- {
---   "time-format": "dd.MM.yyyy HH:mm",
---   "blog-title": "Murja.dev @ roland",
---   "recent-post-count": 6,
---   "xss-filter-posts?": false
--- }
-
-
 module Settings exposing (..)
 
 import Json.Decode as Decode exposing (Decoder, succeed)
 import Json.Decode.Pipeline exposing (required)
 import Json.Decode.Extra as Extra
-
-import Article
+import Json.Encode as Json exposing (..)
 
 type alias Settings =
     { time_format : String
     , blog_title : String
-    , recent_post_count : Int
-    , xss_filter_posts : Bool
-    }
+    , recent_post_count : Int}
 
-settingsDecoder = Decode.map4 Settings
+settingsDecoder = Decode.map3 Settings
                   (Decode.field "time-format" Decode.string)
                   (Decode.field "blog-title" Decode.string)
                   (Decode.field "recent-post-count" Decode.int)
-                  (Decode.field "xss-filter-posts?" Decode.bool)
                      
+encodeSettings settings =
+    object
+        [ ( "time-format", string settings.time_format )
+        , ( "blog-title", string settings.blog_title)
+        , ( "recent-post-count", int settings.recent_post_count)]
diff --git a/elm-frontti/src/SettingsEditor.elm b/elm-frontti/src/SettingsEditor.elm
new file mode 100644
index 0000000..226aa6c
--- /dev/null
+++ b/elm-frontti/src/SettingsEditor.elm
@@ -0,0 +1,32 @@
+module SettingsEditor exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Json.Decode as D
+
+import Message exposing (..)
+
+editor settings =
+    div [ class "form-grid" ]
+        [ label [ for "time-format"]
+              [ text "Time format "]
+        , input [ id "time-format"
+                , onInput SetTimeFormat 
+                , value settings.time_format] []
+            
+        , label [ for "title" ]
+            [ text "Title" ]
+        , input [ id "title"
+                , onInput SetBlogTitle 
+                , value settings.blog_title] []
+
+        , label [ for "page_size" ]
+            [ text "Page size (posts)"]
+        , input [ id "page_size"
+                , onInput SetPageSize 
+                , value (String.fromInt settings.recent_post_count)
+                , type_ "number"] []
+
+        , button [ onClick SaveSettings ]
+            [ text "Save settings"]]
diff --git a/elm-frontti/src/Topbar.elm b/elm-frontti/src/Topbar.elm
index af39075..a8e0993 100644
--- a/elm-frontti/src/Topbar.elm
+++ b/elm-frontti/src/Topbar.elm
@@ -13,18 +13,23 @@ import Browser.Navigation as Nav
 
 import Button exposing (murja_button)
 
+topbar_list =
+    [ li [] [ murja_button [ onClick GoHome, attribute "data-testid" "home"]
+                  [text "Home"]]
+    , li [] [ murja_button [ onClick (PushUrl "/blog/postadmin"), attribute "data-testid" "manage-posts-btn" ]
+                  [text "Manage posts"]]
+    , li [] [ murja_button [ onClick (PushUrl "/blog/mediamanager")]
+                  [text "Manage media"]]
+    , li [] [ murja_button [ onClick (PushUrl "/blog/settings")]
+                  [ text "Settings" ]]
+    , li [] [ murja_button [ onClick GenNewPost
+                           , attribute "data-testid" "new-post-btn" ]
+                  [text "New post!"]]]
+    
 topbar state =
     case state of
         LoggedIn user ->                               
             div [class "left-sidebar"] [ span [] [text ("Welcome, " ++ user.nickname)]
                                        , User.user_avatar user
-                                       , ul [] [ li [] [ murja_button [ onClick GoHome, attribute "data-testid" "home"]
-                                                             [text "Home"]]
-                                               , li [] [ murja_button [ onClick (PushUrl "/blog/postadmin"), attribute "data-testid" "manage-posts-btn" ]
-                                                             [text "Manage posts"]]
-                                               , li [] [ murja_button [ onClick (PushUrl "/blog/mediamanager")]
-                                                             [text "Manage media"]]
-                                               , li [] [ murja_button [ onClick GenNewPost
-                                                                      , attribute "data-testid" "new-post-btn" ]
-                                                             [text "New post!"]]]]
+                                       , ul [] topbar_list ]
         _ -> div [] []
diff --git a/resources/css/murja.css b/resources/css/murja.css
index 6e5bac7..2e5eb73 100644
--- a/resources/css/murja.css
+++ b/resources/css/murja.css
@@ -249,6 +249,22 @@ header {
     display: block;
 }
 
+.form-grid
+{
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 5px;
+    height: fit-content;
+}
+
+.form-grid label {
+    grid-column: 1;
+}  
+
+.form-grid input {
+    grid-column: 2;
+}
+
 
 @media only screen and (max-device-width:480px)
 {
diff --git a/resources/sql/017-settings-in-db.sql b/resources/sql/017-settings-in-db.sql
new file mode 100644
index 0000000..caf1362
--- /dev/null
+++ b/resources/sql/017-settings-in-db.sql
@@ -0,0 +1,16 @@
+CREATE TABLE IF NOT EXISTS blog.Settings
+(
+	key TEXT NOT NULL PRIMARY KEY,
+	value JSONB NOT NULL
+);
+
+INSERT INTO blog.Settings VALUES ('time-format', '"dd.MM.yyyy HH:mm"'),
+                                 ('blog-title', '"Murja.dev @ $HOSTNAME"'),
+                                 ('recent-post-count', '6')
+ON CONFLICT DO NOTHING;
+--for reasons unknown SERIAL is broken
+-- but then, the id is supposed to be stable, so this should be fine
+INSERT INTO blog.Permission (id, action) VALUES (13, 'update-settings') ON CONFLICT DO NOTHING;
+
+INSERT INTO blog.GroupPermissions VALUES ((select id from blog.Permission where action = 'update-settings')
+       	    			  	 , (select id from blog.UserGroup where name = 'Admins')) ON CONFLICT DO NOTHING;
diff --git a/src/migration-list.lisp b/src/migration-list.lisp
index 9e8321e..740fbf7 100644
--- a/src/migration-list.lisp
+++ b/src/migration-list.lisp
@@ -21,6 +21,7 @@
 (defmigration "014-tag-hidden-unlisted-validator.up")
 (defmigration "015-image-post-pairing-view.up")
 (defmigration "016-hardcoded-hidden-unlisted")
+(defmigration "017-settings-in-db")
 
 (defun prepare-e2e-migration ()
   (postmodern:execute "DELETE FROM blog.Users")
diff --git a/src/routes/root-routes.lisp b/src/routes/root-routes.lisp
index 43bf373..1efabb1 100644
--- a/src/routes/root-routes.lisp
+++ b/src/routes/root-routes.lisp
@@ -50,10 +50,6 @@
 	  ((string= type "css") "text/css")
 	  (t (error 'unknown-mime :file-type type)))))
 
-(defroute client-settings ("/api/settings/client-settings" :method :get
-							   :decorators (@json)) ()
-  "{\"time-format\":\"dd.MM.yyyy HH:mm\",\"blog-title\":\"Murja.dev @ $HOSTNAME\",\"recent-post-count\":6,\"xss-filter-posts?\":false}")
-
 (defun get-resource (file)
   (let ((path (gethash file *allowed-resources*)))
     (if path
@@ -115,3 +111,6 @@
 
 (defroute sdkfpsokopfs ("/blog/post/:post/version/:ver" :method :get) ()
   *root*)
+
+(defroute ddddddd ("/blog/settings" :method :get) ()
+  *root*)
diff --git a/src/routes/settings-routes.lisp b/src/routes/settings-routes.lisp
new file mode 100644
index 0000000..593b40f
--- /dev/null
+++ b/src/routes/settings-routes.lisp
@@ -0,0 +1,35 @@
+(defpackage murja.routes.settings-routes
+  (:use :cl)
+  (:import-from :com.inuoe.jzon :stringify :parse)
+  (:import-from :murja.middleware.auth :@authenticated :*user* :@can?)
+  (:import-from :murja.middleware.json :@json)
+  (:import-from :murja.middleware.db :@transaction)
+  (:import-from :easy-routes :defroute))
+
+(in-package :murja.routes.settings-routes)
+
+(defroute client-settings ("/api/settings/client-settings" :method :get
+							   :decorators (@transaction
+									@json)) ()
+  (com.inuoe.jzon:stringify
+   (reduce (lambda (acc pair)
+	     (destructuring-bind (k v) pair
+	       (setf (gethash k acc) (com.inuoe.jzon:parse v))
+	       acc)) 
+	   (postmodern:query "SELECT key, value FROM blog.Settings")
+
+	   :initial-value (make-hash-table))))
+
+(defroute update-setting ("/api/settings/client-settings" :method :put 
+							  :decorators (@transaction
+								       @json
+								       @authenticated
+								       (@can? "update-settings"))) ()
+  (let ((req (alexandria:hash-table-alist
+	      (parse (hunchentoot:raw-post-data :force-text t)))))
+    (dolist (p req)
+      (destructuring-bind (k . v) p
+	(format t "execute returned for ~a => ~a: ~a~%" k v
+		(postmodern:execute "UPDATE blog.Settings SET value = $2 WHERE key = $1" k (stringify v)))))
+    (setf (hunchentoot:return-code*) 204)
+    ""))