all repos

onasty @ 58c535a5aae5dee03a5cbcb2efc82e1426dcea76

a one-time notes service
16 files changed, 268 insertions(+), 72 deletions(-)
web: add some styles (#136)

* web: setup postcss

* web: delete debug things from home page

* web: refactor how user is stored

* web: add shared header

* web: set theme to light

* web(header): change the list of links that shown to user based on status

* web: fix lining errors

* web: change the look of auth form

* chore(ci): check the formatting of elm code

* fixup! web: fix lining errors
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-06-19 19:46:27 +0300
Parent: 8ffca5c
M .github/workflows/elm.yml
···
        36
        36
               - name: elm-review

      
        37
        37
                 run: bunx elm-review --ignore-dirs .elm-land

      
        38
        38
         

      
        
        39
        +      - name: elm-format

      
        
        40
        +        run: bunx elm-format --validate src/

      
        
        41
        +

      
        
        42
        +

      
        39
        43
               - name: Tests

      
        40
        44
                 run: bunx elm-test-rs

      
        41
        45
         

      
A web/.postcssrc.json
···
        
        1
        +{

      
        
        2
        +  "plugins": {

      
        
        3
        +    "postcss-import": {},

      
        
        4
        +    "autoprefixer": {}

      
        
        5
        +  }

      
        
        6
        +}

      
M web/bun.lock
···
        4
        4
             "": {

      
        5
        5
               "name": "web",

      
        6
        6
               "devDependencies": {

      
        
        7
        +        "autoprefixer": "^10.4.21",

      
        7
        8
                 "elm-land": "^0.20.1",

      
        8
        9
                 "elm-review": "^2.13.2",

      
        9
        10
                 "elm-tooling": "^1.15.1",

      
        
        11
        +        "missing.css": "^1.1.3",

      
        
        12
        +        "postcss": "^8.5.6",

      
        
        13
        +        "postcss-import": "^16.1.1",

      
        10
        14
               },

      
        11
        15
             },

      
        12
        16
           },

      ···
        157
        161
         

      
        158
        162
             "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],

      
        159
        163
         

      
        
        164
        +    "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],

      
        
        165
        +

      
        160
        166
             "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],

      
        161
        167
         

      
        162
        168
             "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],

      ···
        169
        175
         

      
        170
        176
             "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],

      
        171
        177
         

      
        
        178
        +    "browserslist": ["browserslist@4.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="],

      
        
        179
        +

      
        172
        180
             "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],

      
        173
        181
         

      
        174
        182
             "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],

      ···
        177
        185
         

      
        178
        186
             "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],

      
        179
        187
         

      
        
        188
        +    "caniuse-lite": ["caniuse-lite@1.0.30001723", "", {}, "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw=="],

      
        
        189
        +

      
        180
        190
             "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],

      
        181
        191
         

      
        182
        192
             "chokidar": ["chokidar@3.5.3", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw=="],

      ···
        207
        217
         

      
        208
        218
             "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],

      
        209
        219
         

      
        
        220
        +    "electron-to-chromium": ["electron-to-chromium@1.5.170", "", {}, "sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA=="],

      
        
        221
        +

      
        210
        222
             "elm": ["elm@0.19.1-6", "", { "optionalDependencies": { "@elm_binaries/darwin_arm64": "0.19.1-0", "@elm_binaries/darwin_x64": "0.19.1-0", "@elm_binaries/linux_x64": "0.19.1-0", "@elm_binaries/win32_x64": "0.19.1-0" }, "bin": { "elm": "bin/elm" } }, "sha512-mKYyierHICPdMx/vhiIacdPmTPnh889gjHOZ75ZAoCxo3lZmSWbGP8HMw78wyctJH0HwvTmeKhlYSWboQNYPeQ=="],

      
        211
        223
         

      
        212
        224
             "elm-land": ["elm-land@0.20.1", "", { "dependencies": { "@lydell/elm": "0.19.1-14", "chokidar": "3.5.3", "terser": "5.15.1", "typescript": "4.9.3", "vite": "5.2.8", "vite-plugin-elm-watch": "1.3.3" }, "bin": { "elm-land": "src/index.js" } }, "sha512-AY8BxYNT7mblaIO9SS2YQPdskZdMsLL6fqjAA5bORdkGIRDkMeaw+rXgiVSHUM2+TK0k/ld0TdQEAd24Moi5nw=="],

      ···
        223
        235
         

      
        224
        236
             "esbuild": ["esbuild@0.20.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.20.2", "@esbuild/android-arm": "0.20.2", "@esbuild/android-arm64": "0.20.2", "@esbuild/android-x64": "0.20.2", "@esbuild/darwin-arm64": "0.20.2", "@esbuild/darwin-x64": "0.20.2", "@esbuild/freebsd-arm64": "0.20.2", "@esbuild/freebsd-x64": "0.20.2", "@esbuild/linux-arm": "0.20.2", "@esbuild/linux-arm64": "0.20.2", "@esbuild/linux-ia32": "0.20.2", "@esbuild/linux-loong64": "0.20.2", "@esbuild/linux-mips64el": "0.20.2", "@esbuild/linux-ppc64": "0.20.2", "@esbuild/linux-riscv64": "0.20.2", "@esbuild/linux-s390x": "0.20.2", "@esbuild/linux-x64": "0.20.2", "@esbuild/netbsd-x64": "0.20.2", "@esbuild/openbsd-x64": "0.20.2", "@esbuild/sunos-x64": "0.20.2", "@esbuild/win32-arm64": "0.20.2", "@esbuild/win32-ia32": "0.20.2", "@esbuild/win32-x64": "0.20.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g=="],

      
        225
        237
         

      
        
        238
        +    "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],

      
        
        239
        +

      
        226
        240
             "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="],

      
        227
        241
         

      
        228
        242
             "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],

      ···
        233
        247
         

      
        234
        248
             "folder-hash": ["folder-hash@3.3.3", "", { "dependencies": { "debug": "^4.1.1", "graceful-fs": "~4.2.0", "minimatch": "~3.0.4" }, "bin": { "folder-hash": "bin/folder-hash" } }, "sha512-SDgHBgV+RCjrYs8aUwCb9rTgbTVuSdzvFmLaChsLre1yf+D64khCW++VYciaByZ8Rm0uKF8R/XEpXuTRSGUM1A=="],

      
        235
        249
         

      
        
        250
        +    "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],

      
        
        251
        +

      
        236
        252
             "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],

      
        
        253
        +

      
        
        254
        +    "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],

      
        237
        255
         

      
        238
        256
             "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],

      
        239
        257
         

      ···
        245
        263
         

      
        246
        264
             "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],

      
        247
        265
         

      
        
        266
        +    "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],

      
        
        267
        +

      
        248
        268
             "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],

      
        249
        269
         

      
        250
        270
             "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],

      ···
        254
        274
             "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],

      
        255
        275
         

      
        256
        276
             "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],

      
        
        277
        +

      
        
        278
        +    "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],

      
        257
        279
         

      
        258
        280
             "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],

      
        259
        281
         

      ···
        291
        313
         

      
        292
        314
             "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],

      
        293
        315
         

      
        
        316
        +    "missing.css": ["missing.css@1.1.3", "", {}, "sha512-dkGzliE9Zcv9tGPvuIHy1lpMc4Y5QAc+svF7Nqi+EMl4taRC5HfjARg55YC2jC9cbqOwX0qOUltUxqn57YIQiw=="],

      
        
        317
        +

      
        294
        318
             "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],

      
        295
        319
         

      
        296
        320
             "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],

      
        297
        321
         

      
        
        322
        +    "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],

      
        
        323
        +

      
        298
        324
             "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],

      
        
        325
        +

      
        
        326
        +    "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],

      
        299
        327
         

      
        300
        328
             "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],

      
        301
        329
         

      ···
        315
        343
         

      
        316
        344
             "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],

      
        317
        345
         

      
        
        346
        +    "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],

      
        
        347
        +

      
        318
        348
             "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],

      
        319
        349
         

      
        320
        350
             "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],

      
        321
        351
         

      
        
        352
        +    "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],

      
        
        353
        +

      
        322
        354
             "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],

      
        323
        355
         

      
        
        356
        +    "postcss-import": ["postcss-import@16.1.1", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ=="],

      
        
        357
        +

      
        
        358
        +    "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],

      
        
        359
        +

      
        324
        360
             "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],

      
        325
        361
         

      
        326
        362
             "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],

      
        327
        363
         

      
        328
        364
             "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],

      
        329
        365
         

      
        
        366
        +    "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],

      
        
        367
        +

      
        330
        368
             "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],

      
        331
        369
         

      
        332
        370
             "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],

      
        
        371
        +

      
        
        372
        +    "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],

      
        333
        373
         

      
        334
        374
             "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],

      
        335
        375
         

      ···
        367
        407
         

      
        368
        408
             "supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="],

      
        369
        409
         

      
        
        410
        +    "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],

      
        
        411
        +

      
        370
        412
             "terminal-link": ["terminal-link@2.1.1", "", { "dependencies": { "ansi-escapes": "^4.2.1", "supports-hyperlinks": "^2.0.0" } }, "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ=="],

      
        371
        413
         

      
        372
        414
             "terser": ["terser@5.15.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw=="],

      ···
        382
        424
             "typescript": ["typescript@4.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA=="],

      
        383
        425
         

      
        384
        426
             "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],

      
        
        427
        +

      
        
        428
        +    "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],

      
        385
        429
         

      
        386
        430
             "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],

      
        387
        431
         

      
M web/elm-land.json
···
        12
        12
             "html": {

      
        13
        13
               "attributes": {

      
        14
        14
                 "html": {

      
        15
        
        -          "lang": "en"

      
        
        15
        +          "lang": "en",

      
        
        16
        +          "class": "-no-dark-theme"

      
        16
        17
                 },

      
        17
        18
                 "head": {}

      
        18
        19
               },

      
M web/package.json
···
        4
        4
             "postinstall": "elm-tooling install"

      
        5
        5
           },

      
        6
        6
           "devDependencies": {

      
        
        7
        +    "autoprefixer": "^10.4.21",

      
        7
        8
             "elm-land": "^0.20.1",

      
        8
        9
             "elm-review": "^2.13.2",

      
        9
        
        -    "elm-tooling": "^1.15.1"

      
        
        10
        +    "elm-tooling": "^1.15.1",

      
        
        11
        +    "missing.css": "^1.1.3",

      
        
        12
        +    "postcss": "^8.5.6",

      
        
        13
        +    "postcss-import": "^16.1.1"

      
        10
        14
           }

      
        11
        15
         }

      
M web/src/Auth.elm
···
        1
        1
         module Auth exposing (User, onPageLoad, viewCustomPage)

      
        2
        2
         

      
        3
        3
         import Auth.Action

      
        
        4
        +import Auth.User

      
        4
        5
         import Dict

      
        5
        6
         import Route exposing (Route)

      
        6
        7
         import Route.Path

      ···
        9
        10
         

      
        10
        11
         

      
        11
        12
         type alias User =

      
        12
        
        -    { accessToken : String

      
        13
        
        -    , refreshToken : String

      
        14
        
        -    }

      
        
        13
        +    Auth.User.User

      
        15
        14
         

      
        16
        15
         

      
        17
        16
         {-| Called before an auth-only page is loaded.

      
        18
        17
         -}

      
        19
        18
         onPageLoad : Shared.Model -> Route () -> Auth.Action.Action User

      
        20
        19
         onPageLoad shared _ =

      
        21
        
        -    case shared.credentials of

      
        22
        
        -        Just credentials ->

      
        23
        
        -            Auth.Action.loadPageWithUser

      
        24
        
        -                { accessToken = credentials.accessToken

      
        25
        
        -                , refreshToken = credentials.refreshToken

      
        26
        
        -                }

      
        27
        
        -

      
        28
        
        -        _ ->

      
        
        20
        +    case shared.user of

      
        
        21
        +        Auth.User.NotSignedIn ->

      
        29
        22
                     Auth.Action.pushRoute

      
        30
        23
                         { path = Route.Path.Auth

      
        31
        24
                         , query = Dict.empty

      
        32
        25
                         , hash = Nothing

      
        33
        26
                         }

      
        
        27
        +

      
        
        28
        +        Auth.User.SignedIn credentials ->

      
        
        29
        +            Auth.Action.loadPageWithUser credentials

      
        34
        30
         

      
        35
        31
         

      
        36
        32
         {-| Renders whenever `Auth.Action.loadCustomPage` is returned from `onPageLoad`.

      
A web/src/Auth/User.elm
···
        
        1
        +module Auth.User exposing (SignInStatus(..), User)

      
        
        2
        +

      
        
        3
        +

      
        
        4
        +type alias User =

      
        
        5
        +    { accessToken : String

      
        
        6
        +    , refreshToken : String

      
        
        7
        +    }

      
        
        8
        +

      
        
        9
        +

      
        
        10
        +type SignInStatus

      
        
        11
        +    = SignedIn User

      
        
        12
        +    | NotSignedIn

      
M web/src/Effect.elm
···
        5
        5
             , pushRoute, replaceRoute

      
        6
        6
             , pushRoutePath, replaceRoutePath

      
        7
        7
             , loadExternalUrl, back

      
        8
        
        -    , sendApiRequest

      
        
        8
        +    , sendApiRequest, refreshTokens

      
        9
        9
             , signin, logout, saveUser, clearUser

      
        10
        
        -    , map, toCmd, refreshTokens

      
        
        10
        +    , map, toCmd

      
        11
        11
             )

      
        12
        12
         

      
        13
        13
         {-|

      ···
        29
        29
         -}

      
        30
        30
         

      
        31
        31
         import Api exposing (HttpRequestDetails)

      
        
        32
        +import Auth.User

      
        32
        33
         import Browser.Navigation

      
        33
        34
         import Data.Credentials exposing (Credentials)

      
        34
        35
         import Dict exposing (Dict)

      ···
        308
        309
                     let

      
        309
        310
                         headers : List Http.Header

      
        310
        311
                         headers =

      
        311
        
        -                    case options.shared.credentials of

      
        312
        
        -                        Just tok ->

      
        
        312
        +                    case options.shared.user of

      
        
        313
        +                        Auth.User.SignedIn cred ->

      
        313
        314
                                     if not (String.contains opts.endpoint "refresh-tokens") then

      
        314
        
        -                                [ Http.header "Authorization" ("Bearer " ++ tok.accessToken) ]

      
        
        315
        +                                [ Http.header "Authorization" ("Bearer " ++ cred.accessToken) ]

      
        315
        316
         

      
        316
        317
                                     else

      
        317
        318
                                         []

      
        318
        319
         

      
        319
        
        -                        Nothing ->

      
        
        320
        +                        Auth.User.NotSignedIn ->

      
        320
        321
                                     []

      
        321
        322
                     in

      
        322
        323
                     Http.request

      
A web/src/Layouts/Header.elm
···
        
        1
        +module Layouts.Header exposing (Model, Msg, Props, layout)

      
        
        2
        +

      
        
        3
        +import Auth.User

      
        
        4
        +import Effect exposing (Effect)

      
        
        5
        +import Html exposing (Html)

      
        
        6
        +import Html.Attributes as Attr

      
        
        7
        +import Html.Events

      
        
        8
        +import Layout exposing (Layout)

      
        
        9
        +import Route exposing (Route)

      
        
        10
        +import Route.Path

      
        
        11
        +import Shared

      
        
        12
        +import View exposing (View)

      
        
        13
        +

      
        
        14
        +

      
        
        15
        +type alias Props =

      
        
        16
        +    {}

      
        
        17
        +

      
        
        18
        +

      
        
        19
        +layout : Props -> Shared.Model -> Route () -> Layout () Model Msg contentMsg

      
        
        20
        +layout _ shared _ =

      
        
        21
        +    Layout.new

      
        
        22
        +        { init = init

      
        
        23
        +        , update = update

      
        
        24
        +        , view = view shared

      
        
        25
        +        , subscriptions = subscriptions

      
        
        26
        +        }

      
        
        27
        +

      
        
        28
        +

      
        
        29
        +

      
        
        30
        +-- MODEL

      
        
        31
        +

      
        
        32
        +

      
        
        33
        +type alias Model =

      
        
        34
        +    {}

      
        
        35
        +

      
        
        36
        +

      
        
        37
        +init : () -> ( Model, Effect Msg )

      
        
        38
        +init _ =

      
        
        39
        +    ( {}, Effect.none )

      
        
        40
        +

      
        
        41
        +

      
        
        42
        +

      
        
        43
        +-- UPDATE

      
        
        44
        +

      
        
        45
        +

      
        
        46
        +type Msg

      
        
        47
        +    = UserClickedLogout

      
        
        48
        +

      
        
        49
        +

      
        
        50
        +update : Msg -> Model -> ( Model, Effect Msg )

      
        
        51
        +update msg model =

      
        
        52
        +    case msg of

      
        
        53
        +        UserClickedLogout ->

      
        
        54
        +            ( model, Effect.logout )

      
        
        55
        +

      
        
        56
        +

      
        
        57
        +subscriptions : Model -> Sub Msg

      
        
        58
        +subscriptions _ =

      
        
        59
        +    Sub.none

      
        
        60
        +

      
        
        61
        +

      
        
        62
        +

      
        
        63
        +-- VIEW

      
        
        64
        +

      
        
        65
        +

      
        
        66
        +view : Shared.Model -> { toContentMsg : Msg -> contentMsg, content : View contentMsg, model : Model } -> View contentMsg

      
        
        67
        +view shared { toContentMsg, content } =

      
        
        68
        +    { title = content.title

      
        
        69
        +    , body =

      
        
        70
        +        [ viewNavbar shared |> Html.map toContentMsg

      
        
        71
        +        , Html.main_ [] content.body

      
        
        72
        +        ]

      
        
        73
        +    }

      
        
        74
        +

      
        
        75
        +

      
        
        76
        +viewNavbar : Shared.Model -> Html Msg

      
        
        77
        +viewNavbar shared =

      
        
        78
        +    Html.header [ Attr.class "navbar" ]

      
        
        79
        +        [ Html.nav [ Attr.class "f-row justify-content:space-between" ]

      
        
        80
        +            [ Html.ul [ Attr.attribute "role" "list" ]

      
        
        81
        +                [ Html.li [] [ viewNavLink ( "home", Route.Path.Home_ ) ] ]

      
        
        82
        +            , Html.ul [ Attr.attribute "role" "list" ]

      
        
        83
        +                (case shared.user of

      
        
        84
        +                    Auth.User.SignedIn _ ->

      
        
        85
        +                        [ Html.li [] [ viewNavLink ( "profile", Route.Path.Profile_Me ) ]

      
        
        86
        +                        , Html.li [] [ Html.a [ Html.Events.onClick UserClickedLogout ] [ Html.text "logout" ] ]

      
        
        87
        +                        ]

      
        
        88
        +

      
        
        89
        +                    Auth.User.NotSignedIn ->

      
        
        90
        +                        [ Html.li [] [ viewNavLink ( "sign in", Route.Path.Auth ) ]

      
        
        91
        +                        ]

      
        
        92
        +                )

      
        
        93
        +            ]

      
        
        94
        +        ]

      
        
        95
        +

      
        
        96
        +

      
        
        97
        +viewNavLink : ( String, Route.Path.Path ) -> Html msg

      
        
        98
        +viewNavLink ( label, path ) =

      
        
        99
        +    Html.a

      
        
        100
        +        [ Route.Path.href path ]

      
        
        101
        +        [ Html.text label ]

      
M web/src/Pages/Auth.elm
···
        2
        2
         

      
        3
        3
         import Api

      
        4
        4
         import Api.Auth

      
        
        5
        +import Auth.User

      
        5
        6
         import Data.Credentials exposing (Credentials)

      
        6
        7
         import Effect exposing (Effect)

      
        7
        8
         import Html exposing (Html)

      
        8
        9
         import Html.Attributes as Attr

      
        9
        10
         import Html.Events

      
        10
        11
         import Http

      
        
        12
        +import Layouts

      
        11
        13
         import Page exposing (Page)

      
        12
        14
         import Route exposing (Route)

      
        13
        15
         import Route.Path

      ···
        23
        25
                 , subscriptions = subscriptions

      
        24
        26
                 , view = view

      
        25
        27
                 }

      
        
        28
        +        |> Page.withLayout (\_ -> Layouts.Header {})

      
        26
        29
         

      
        27
        30
         

      
        28
        31
         

      ···
        48
        51
               , formVariant = SignIn

      
        49
        52
               , error = Nothing

      
        50
        53
               }

      
        51
        
        -    , case shared.credentials of

      
        52
        
        -        Just _ ->

      
        
        54
        +    , case shared.user of

      
        
        55
        +        Auth.User.SignedIn _ ->

      
        53
        56
                     Effect.pushRoutePath Route.Path.Home_

      
        54
        57
         

      
        55
        
        -        Nothing ->

      
        
        58
        +        Auth.User.NotSignedIn ->

      
        56
        59
                     Effect.none

      
        57
        60
             )

      
        58
        61
         

      ···
        146
        149
         view model =

      
        147
        150
             { title = "Authentication"

      
        148
        151
             , body =

      
        149
        
        -        [ Html.div []

      
        
        152
        +        [ Html.div [ Attr.class "center" ]

      
        150
        153
                     -- TODO: add oauth buttons

      
        151
        
        -            [ viewChangeVariant model.formVariant

      
        152
        
        -            , viewError model.error

      
        
        154
        +            [ viewError model.error

      
        
        155
        +            , viewChangeVariant model.formVariant

      
        153
        156
                     , viewForm model

      
        
        157
        +            , viewForgotPassword

      
        154
        158
                     ]

      
        155
        159
                 ]

      
        156
        160
             }

      ···
        158
        162
         

      
        159
        163
         viewChangeVariant : Variant -> Html Msg

      
        160
        164
         viewChangeVariant variant =

      
        161
        
        -    Html.div []

      
        
        165
        +    Html.div [ Attr.class "mb1" ]

      
        162
        166
                 [ Html.button

      
        163
        167
                     [ Attr.disabled (variant == SignIn)

      
        164
        168
                     , Html.Events.onClick (UserChangedFormVariant SignIn)

      ···
        179
        183
                     SignIn ->

      
        180
        184
                         [ viewFormInput { field = Email, value = model.email }

      
        181
        185
                         , viewFormInput { field = Password, value = model.password }

      
        182
        
        -                , viewFormControls model

      
        
        186
        +                , viewSubmitButton model

      
        183
        187
                         ]

      
        184
        188
         

      
        185
        189
                     SignUp ->

      
        186
        190
                         [ viewFormInput { field = Email, value = model.email }

      
        187
        191
                         , viewFormInput { field = Password, value = model.password }

      
        188
        192
                         , viewFormInput { field = PasswordAgain, value = model.passwordAgain }

      
        189
        
        -                , viewFormControls model

      
        
        193
        +                , viewSubmitButton model

      
        190
        194
                         ]

      
        191
        195
                 )

      
        192
        196
         

      ···
        195
        199
         viewError maybeError =

      
        196
        200
             case maybeError of

      
        197
        201
                 Just error ->

      
        198
        
        -            Html.div [ Attr.style "color" "red" ]

      
        199
        
        -                [ Html.text (Api.errorToFriendlyMessage error) ]

      
        
        202
        +            Html.div [ Attr.class "box bad" ]

      
        
        203
        +                [ Html.strong [ Attr.class "block titlebar" ] [ Html.text "Error" ]

      
        
        204
        +                , Html.text (Api.errorToFriendlyMessage error)

      
        
        205
        +                ]

      
        200
        206
         

      
        201
        207
                 Nothing ->

      
        202
        208
                     Html.text ""

      ···
        204
        210
         

      
        205
        211
         viewFormInput : { field : Field, value : String } -> Html Msg

      
        206
        212
         viewFormInput opts =

      
        207
        
        -    Html.div []

      
        
        213
        +    Html.div [ Attr.class "mb1" ]

      
        208
        214
                 [ Html.label [] [ Html.text (fromFieldToLabel opts.field) ]

      
        209
        215
                 , Html.div []

      
        210
        216
                     [ Html.input

      ···
        217
        223
                 ]

      
        218
        224
         

      
        219
        225
         

      
        220
        
        -viewFormControls : Model -> Html Msg

      
        221
        
        -viewFormControls model =

      
        
        226
        +viewForgotPassword : Html Msg

      
        
        227
        +viewForgotPassword =

      
        222
        228
             Html.div []

      
        
        229
        +        [ Html.a

      
        
        230
        +            [ Attr.href "/forgot-password"

      
        
        231
        +            , Attr.class "gray"

      
        
        232
        +            ]

      
        
        233
        +            [ Html.text "Forgot password?" ]

      
        
        234
        +        ]

      
        
        235
        +

      
        
        236
        +

      
        
        237
        +viewSubmitButton : Model -> Html Msg

      
        
        238
        +viewSubmitButton model =

      
        
        239
        +    Html.div [ Attr.class "mb1" ]

      
        223
        240
                 [ Html.button

      
        224
        241
                     [ Attr.disabled (isFormDisabled model) ]

      
        225
        
        -            (case model.formVariant of

      
        226
        
        -                SignIn ->

      
        227
        
        -                    [ Html.text "Sign In" ]

      
        228
        
        -

      
        229
        
        -                SignUp ->

      
        230
        
        -                    [ Html.text "Sign Up" ]

      
        231
        
        -            )

      
        
        242
        +            [ Html.text (fromVariantToLabel model.formVariant) ]

      
        232
        243
                 ]

      
        233
        244
         

      
        234
        245
         

      ···
        246
        257
                         || String.isEmpty model.password

      
        247
        258
                         || String.isEmpty model.passwordAgain

      
        248
        259
                         || (model.password /= model.passwordAgain)

      
        
        260
        +

      
        
        261
        +

      
        
        262
        +fromVariantToLabel : Variant -> String

      
        
        263
        +fromVariantToLabel variant =

      
        
        264
        +    case variant of

      
        
        265
        +        SignIn ->

      
        
        266
        +            "Sign In"

      
        
        267
        +

      
        
        268
        +        SignUp ->

      
        
        269
        +            "Sign Up"

      
        249
        270
         

      
        250
        271
         

      
        251
        272
         fromFieldToLabel : Field -> String

      
M web/src/Pages/Home_.elm
···
        2
        2
         

      
        3
        3
         import Effect exposing (Effect)

      
        4
        4
         import Html

      
        5
        
        -import Html.Attributes as Attributes

      
        6
        5
         import Html.Events

      
        
        6
        +import Layouts

      
        7
        7
         import Page exposing (Page)

      
        8
        8
         import Route exposing (Route)

      
        9
        9
         import Shared

      ···
        18
        18
                 , subscriptions = subscriptions

      
        19
        19
                 , view = view shared

      
        20
        20
                 }

      
        
        21
        +        |> Page.withLayout Layouts.Header

      
        21
        22
         

      
        22
        23
         

      
        23
        24
         

      ···
        38
        39
         

      
        39
        40
         

      
        40
        41
         type Msg

      
        41
        
        -    = LogOut

      
        
        42
        +    = NoOp

      
        42
        43
         

      
        43
        44
         

      
        44
        45
         update : Msg -> Model -> ( Model, Effect Msg )

      
        45
        46
         update msg model =

      
        46
        47
             case msg of

      
        47
        
        -        LogOut ->

      
        48
        
        -            ( model, Effect.logout )

      
        
        48
        +        NoOp ->

      
        
        49
        +            ( model, Effect.none )

      
        49
        50
         

      
        50
        51
         

      
        51
        52
         

      ···
        64
        65
         view : Shared.Model -> Model -> View Msg

      
        65
        66
         view _ _ =

      
        66
        67
             { title = "Homepage"

      
        67
        
        -    , body =

      
        68
        
        -        [ Html.div []

      
        69
        
        -            [ Html.p [] [ Html.text "Hello, world!" ]

      
        70
        
        -            , Html.p []

      
        71
        
        -                [ Html.a

      
        72
        
        -                    [ Attributes.href "/profile/me" ]

      
        73
        
        -                    [ Html.text "/profile/me - fetches authorized data" ]

      
        74
        
        -                ]

      
        75
        
        -            , Html.p []

      
        76
        
        -                [ Html.button

      
        77
        
        -                    [ Html.Events.onClick LogOut ]

      
        78
        
        -                    [ Html.text "Logout" ]

      
        79
        
        -                ]

      
        80
        
        -            ]

      
        81
        
        -        ]

      
        
        68
        +    , body = [ Html.p [ Html.Events.onClick NoOp ] [ Html.text "Hello, world!" ] ]

      
        82
        69
             }

      
M web/src/Pages/Profile/Me.elm
···
        7
        7
         import Effect exposing (Effect)

      
        8
        8
         import Html exposing (Html)

      
        9
        9
         import Http

      
        
        10
        +import Layouts

      
        10
        11
         import Page exposing (Page)

      
        11
        12
         import Route exposing (Route)

      
        12
        13
         import Shared

      ···
        21
        22
                 , subscriptions = subscriptions

      
        22
        23
                 , view = view shared

      
        23
        24
                 }

      
        
        25
        +        |> Page.withLayout (\_ -> Layouts.Header {})

      
        24
        26
         

      
        25
        27
         

      
        26
        28
         

      
M web/src/Shared.elm
···
        13
        13
         -}

      
        14
        14
         

      
        15
        15
         import Api.Auth

      
        
        16
        +import Auth.User

      
        16
        17
         import Data.Credentials exposing (Credentials)

      
        17
        18
         import Dict

      
        18
        19
         import Effect exposing (Effect)

      ···
        65
        66
                         flags.accessToken

      
        66
        67
                         flags.refreshToken

      
        67
        68
         

      
        
        69
        +        user : Auth.User.SignInStatus

      
        
        70
        +        user =

      
        
        71
        +            case maybeCredentials of

      
        
        72
        +                Just credentials ->

      
        
        73
        +                    Auth.User.SignedIn credentials

      
        
        74
        +

      
        
        75
        +                Nothing ->

      
        
        76
        +                    Auth.User.NotSignedIn

      
        
        77
        +

      
        68
        78
                 initModel : Model

      
        69
        79
                 initModel =

      
        70
        
        -            { credentials = maybeCredentials

      
        
        80
        +            { user = user

      
        71
        81
                     , timeZone = Time.utc

      
        72
        82
                     , isRefreshingTokens = False

      
        73
        83
                     }

      ···
        95
        105
                     ( { model | timeZone = timeZone }, Effect.none )

      
        96
        106
         

      
        97
        107
                 Shared.Msg.Logout ->

      
        98
        
        -            ( { model | credentials = Nothing }, Effect.clearUser )

      
        
        108
        +            ( { model | user = Auth.User.NotSignedIn }, Effect.clearUser )

      
        99
        109
         

      
        100
        110
                 Shared.Msg.SignedIn credentials ->

      
        101
        
        -            ( { model | credentials = Just credentials }

      
        
        111
        +            ( { model | user = Auth.User.SignedIn credentials }

      
        102
        112
                     , Effect.batch

      
        103
        113
                         [ Effect.pushRoute

      
        104
        114
                             { path = Route.Path.Home_

      ···
        110
        120
                     )

      
        111
        121
         

      
        112
        122
                 Shared.Msg.CheckTokenExpiration now ->

      
        113
        
        -            case model.credentials of

      
        114
        
        -                Just credentials ->

      
        
        123
        +            case model.user of

      
        
        124
        +                Auth.User.SignedIn credentials ->

      
        115
        125
                             if JwtUtil.isExpired now credentials.accessToken then

      
        116
        126
                                 ( model, Effect.refreshTokens )

      
        117
        127
         

      
        118
        128
                             else

      
        119
        129
                                 ( model, Effect.none )

      
        120
        130
         

      
        121
        
        -                Nothing ->

      
        
        131
        +                Auth.User.NotSignedIn ->

      
        122
        132
                             ( model, Effect.none )

      
        123
        133
         

      
        124
        134
                 Shared.Msg.TriggerTokenRefresh ->

      
        125
        
        -            case model.credentials of

      
        126
        
        -                Just credentials ->

      
        
        135
        +            case model.user of

      
        
        136
        +                Auth.User.SignedIn credentials ->

      
        127
        137
                             ( { model | isRefreshingTokens = True }

      
        128
        138
                             , Api.Auth.refreshToken

      
        129
        139
                                 { onResponse = Shared.Msg.ApiRefreshTokensResponded

      ···
        131
        141
                                 }

      
        132
        142
                             )

      
        133
        143
         

      
        134
        
        -                Nothing ->

      
        
        144
        +                Auth.User.NotSignedIn ->

      
        135
        145
                             ( model, Effect.none )

      
        136
        146
         

      
        137
        147
                 Shared.Msg.ApiRefreshTokensResponded (Ok credentials) ->

      
        138
        
        -            ( { model | isRefreshingTokens = False, credentials = Just credentials }

      
        
        148
        +            ( { model | isRefreshingTokens = False, user = Auth.User.SignedIn credentials }

      
        139
        149
                     , Effect.saveUser credentials.accessToken credentials.refreshToken

      
        140
        150
                     )

      
        141
        151
         

      
M web/src/Shared/Model.elm
···
        1
        1
         module Shared.Model exposing (Model)

      
        2
        2
         

      
        3
        
        -import Data.Credentials exposing (Credentials)

      
        
        3
        +import Auth.User

      
        4
        4
         import Time

      
        5
        5
         

      
        6
        6
         

      
        7
        7
         type alias Model =

      
        8
        
        -    { credentials : Maybe Credentials

      
        
        8
        +    { user : Auth.User.SignInStatus

      
        9
        9
             , timeZone : Time.Zone

      
        10
        10
             , isRefreshingTokens : Bool

      
        11
        11
             }

      
M web/src/interop.js
···
        
        1
        +import "./styles.css";

      
        
        2
        +

      
        1
        3
         export const flags = (_) => {

      
        2
        4
             return {

      
        3
        5
                 access_token: JSON.parse(window.localStorage.access_token || 'null'),

      ···
        8
        10
         export const onReady = ({ app }) => {

      
        9
        11
             if (app.ports?.sendToLocalStorage) {

      
        10
        12
                 app.ports.sendToLocalStorage.subscribe(({ key, value }) => {

      
        11
        
        -            window.localStorage[key] = JSON.stringify(value)

      
        
        13
        +            window.localStorage[key] = JSON.stringify(value);

      
        12
        14
                 })

      
        13
        15
             }

      
        14
        16
         }

      
A web/src/styles.css
···
        
        1
        +@import "missing.css";

      
        
        2
        +

      
        
        3
        +.mb1 {

      
        
        4
        +  margin-bottom: 1rem;

      
        
        5
        +}