diff --git a/backend/src/botinfo.txt b/backend/src/botinfo.txt new file mode 100644 index 0000000..0753402 --- /dev/null +++ b/backend/src/botinfo.txt @@ -0,0 +1,15 @@ +Hello, if you are reading this, you have found this URL in your +access logs. If this program is doing something you don't want it to do, +please contact me at me@christine.website. + +This service is intended to act as a POSSE[1] syndication server for +various services to various other services. + +Every effort is being taken to ensure that the data going through this +server is my own. + +I'm sorry if this causes you any inconvenience. + +[1]: https://indieweb.org/POSSE + +Be well, Creator. diff --git a/backend/src/main.rs b/backend/src/main.rs index 29f3082..c56bf9d 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -15,21 +15,7 @@ use ::mi::{api, frontend, paseto, rocket_trace::*, web::*, MainDatabase, APPLICA #[get("/.within/botinfo")] fn botinfo() -> &'static str { - r#"Hello, if you are reading this, you have found this URL in your -access logs. If this program is doing something you don't want it to do, -please contact me at me@christine.website. - -This service is intended to act as a POSSE[1] syndication server for -various services to various other services. - -Every effort is being taken to ensure that the data going through this -server is my own. - -I'm sorry if this causes you any inconvenience. - -[1]: https://indieweb.org/POSSE - -Be well, Creator."# + include_str!("./botinfo.txt") } fn main() -> Result<()> { diff --git a/backend/templates/notfound.html b/backend/templates/notfound.html index ef52661..eb2e1f1 100644 --- a/backend/templates/notfound.html +++ b/backend/templates/notfound.html @@ -15,6 +15,18 @@

{{ title }}

{{ message }}

Go home + +
+ + + + diff --git a/shell.nix b/shell.nix index 533024a..ff11663 100644 --- a/shell.nix +++ b/shell.nix @@ -24,6 +24,9 @@ in pkgs.mkShell rec { elm2nix nodePackages.uglify-js + # tools + entr + # keep this line if you use bash bashInteractive ]; diff --git a/sina/scripts/autobuild.sh b/sina/scripts/autobuild.sh new file mode 100755 index 0000000..604b1ed --- /dev/null +++ b/sina/scripts/autobuild.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env nix-shell +#! nix-shell -i bash -p entr + +find ./src/* | entr ./scripts/build-dev.sh diff --git a/sina/scripts/build-dev.sh b/sina/scripts/build-dev.sh index 03647fc..93f3262 100755 --- a/sina/scripts/build-dev.sh +++ b/sina/scripts/build-dev.sh @@ -1,4 +1,6 @@ #!/usr/bin/env nix-shell #! nix-shell -i bash -p elmPackages.elm +echo "--------------- rebuilding ------------------" elm make ./src/Main.elm --output elm.js +echo "------------------ done ---------------------" diff --git a/sina/src/Layout.elm b/sina/src/Layout.elm index 4418832..37e4b72 100644 --- a/sina/src/Layout.elm +++ b/sina/src/Layout.elm @@ -1,10 +1,30 @@ -module Layout exposing (template) +module Layout exposing (basic, template) import Browser exposing (Document) import Html exposing (Html, a, div, h1, main_, nav, text) import Html.Attributes exposing (class, href) +basic : String -> List (Html msg) -> Document msg +basic title body = + { title = title + , body = + [ main_ + [] + ([ nav + [ class "nav" ] + [ a [ href "/" ] [ text "Mi" ] + , text " - " + , a [ href "/login" ] [ text "Login" ] + ] + , h1 [] [ text title ] + ] + ++ body + ) + ] + } + + template : String -> List (Html msg) -> Document msg template title body = { title = title diff --git a/sina/src/Main.elm b/sina/src/Main.elm index e7d7094..9003a47 100644 --- a/sina/src/Main.elm +++ b/sina/src/Main.elm @@ -1,67 +1,146 @@ module Main exposing (main) -import Browser -import Html exposing (Html, div, h1, img, p, pre, text) -import Html.Attributes exposing (src) +import Browser exposing (Document, UrlRequest(..)) +import Browser.Navigation as Nav +import Html exposing (Html, a, br, button, div, h1, img, input, p, pre, span, text) +import Html.Attributes exposing (href, placeholder, src, value) +import Html.Events exposing (onClick, onInput) import Http import Layout import Mi +import Mi.Switch +import Mi.WebMention +import Route exposing (Route(..), routeParser) +import Url exposing (Url) +import Url.Parser as UrlParser exposing (()) -type Model - = Failure String - | Loading - | Success String +{-| All of the data that the app can hold. +-} +type alias Model = + { navKey : Nav.Key + , route : Maybe Route + , token : Maybe String + , tokenData : Maybe Mi.TokenData + , error : Maybe String + } -init : () -> ( Model, Cmd Msg ) -init _ = - ( Loading - , Http.get - { url = "/.within/botinfo" - , expect = Http.expectString GotText - } +init : () -> Url -> Nav.Key -> ( Model, Cmd Msg ) +init _ url key = + ( { navKey = key + , route = UrlParser.parse routeParser url + , token = Nothing + , tokenData = Nothing + , error = Nothing + } + , Cmd.none ) type Msg - = GotText (Result Http.Error String) + = ChangeUrl Url + | ClickLink UrlRequest + | UpdateToken String + | SubmitToken + | ValidateToken (Result Http.Error Mi.TokenData) + | ClearError update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of - GotText result -> + UpdateToken newToken -> + ( { model | token = Just newToken }, Cmd.none ) + + ChangeUrl url -> + ( { model | route = UrlParser.parse routeParser url }, Cmd.none ) + + SubmitToken -> + ( model + , Mi.request + "GET" + (Maybe.withDefault "" model.token) + Mi.tokenIntrospectURL + Http.emptyBody + (Mi.expectJson ValidateToken Mi.tokenDecoder) + ) + + ValidateToken result -> case result of - Ok fullText -> - ( Success fullText, Cmd.none ) + Ok data -> + ( { model | tokenData = Just data } + , Nav.pushUrl model.navKey "/" + ) Err why -> - ( Failure (Mi.errorToString why), Cmd.none ) + ( { model | error = Just <| Mi.errorToString why }, Cmd.none ) + + ClickLink urlRequest -> + case urlRequest of + Internal url -> + ( model, Nav.pushUrl model.navKey <| Url.toString url ) + + External url -> + ( model, Nav.load url ) + + ClearError -> + ( { model | error = Nothing, token = Nothing }, Cmd.none ) -view : Model -> Browser.Document msg +view : Model -> Document Msg view model = - case model of - Failure why -> - Layout.template "Error" + case model.error of + Nothing -> + case Maybe.withDefault Index model.route of + Index -> + case model.tokenData of + Nothing -> + Layout.basic "Login Required" [] + + Just data -> + Layout.template "Mi" + [ p + [] + [ span + [] + [ text "Subscriber: " + , text data.sub + , br [] [] + , text "Token ID: " + , text data.jti + , br [] [] + , text "Issuer: " + , text data.iss + ] + ] + ] + + Login -> + Layout.basic "Login" + [ p [] [ text "Enter the secret code. Unauthorized access is prohibited." ] + , input [ placeholder "API Token", value (Maybe.withDefault "" model.token), onInput UpdateToken ] [] + , button [ onClick SubmitToken ] [ text "Login" ] + ] + + _ -> + Debug.todo "implement routing" + + Just why -> + Layout.basic + "Error" [ p [] [ text why ] - ] - - Loading -> - Layout.template "Loading" [] - - Success msg -> - Layout.template "Mi" - [ pre [] [ text msg ] + , a [ onClick ClearError, href "/" ] [ text "Clear error" ] ] main : Program () Model Msg main = - Browser.document + Browser.application { view = view , init = init , update = update , subscriptions = always Sub.none + , onUrlRequest = ClickLink + , onUrlChange = ChangeUrl } diff --git a/sina/src/Route.elm b/sina/src/Route.elm new file mode 100644 index 0000000..0f16f1f --- /dev/null +++ b/sina/src/Route.elm @@ -0,0 +1,29 @@ +module Route exposing (..) + +import Url.Parser exposing ((), (), Parser, int, map, oneOf, s, string) +import Url.Parser.Query as Query + + +type Route + = Index + | Login + | System + | SwitchLog (Maybe Int) + | SwitchID String + | MakeSwitch + | WebMentionLog (Maybe Int) + | WebMentionID String + + +routeParser : Parser (Route -> a) a +routeParser = + oneOf + [ map Index (s "") + , map Login (s "login") + , map System (s "system") + , map SwitchLog (s "switches" Query.int "page") + , map SwitchID (s "switches" string) + , map MakeSwitch (s "switches" s "log") + , map WebMentionLog (s "webmentions" Query.int "page") + , map WebMentionID (s "webmentions" string) + ] diff --git a/sina/src/serviceWorker.js b/sina/src/serviceWorker.js deleted file mode 100644 index f8c7e50..0000000 --- a/sina/src/serviceWorker.js +++ /dev/null @@ -1,135 +0,0 @@ -// This optional code is used to register a service worker. -// register() is not called by default. - -// This lets the app load faster on subsequent visits in production, and gives -// it offline capabilities. However, it also means that developers (and users) -// will only see deployed updates on subsequent visits to a page, after all the -// existing tabs open on the page have been closed, since previously cached -// resources are updated in the background. - -// To learn more about the benefits of this model and instructions on how to -// opt-in, read https://bit.ly/CRA-PWA - -const isLocalhost = Boolean( - window.location.hostname === 'localhost' || - // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || - // 127.0.0.1/8 is considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) -); - -export function register(config) { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebook/create-react-app/issues/2374 - return; - } - - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; - - if (isLocalhost) { - // This is running on localhost. Let's check if a service worker still exists or not. - checkValidServiceWorker(swUrl, config); - - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://bit.ly/CRA-PWA' - ); - }); - } else { - // Is not localhost. Just register service worker - registerValidSW(swUrl, config); - } - }); - } -} - -function registerValidSW(swUrl, config) { - navigator.serviceWorker - .register(swUrl) - .then(registration => { - registration.onupdatefound = () => { - const installingWorker = registration.installing; - if (installingWorker == null) { - return; - } - installingWorker.onstatechange = () => { - if (installingWorker.state === 'installed') { - if (navigator.serviceWorker.controller) { - // At this point, the updated precached content has been fetched, - // but the previous service worker will still serve the older - // content until all client tabs are closed. - console.log( - 'New content is available and will be used when all ' + - 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' - ); - - // Execute callback - if (config && config.onUpdate) { - config.onUpdate(registration); - } - } else { - // At this point, everything has been precached. - // It's the perfect time to display a - // "Content is cached for offline use." message. - console.log('Content is cached for offline use.'); - - // Execute callback - if (config && config.onSuccess) { - config.onSuccess(registration); - } - } - } - }; - }; - }) - .catch(error => { - console.error('Error during service worker registration:', error); - }); -} - -function checkValidServiceWorker(swUrl, config) { - // Check if the service worker can be found. If it can't reload the page. - fetch(swUrl) - .then(response => { - // Ensure service worker exists, and that we really are getting a JS file. - const contentType = response.headers.get('content-type'); - if ( - response.status === 404 || - (contentType != null && contentType.indexOf('javascript') === -1) - ) { - // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then(registration => { - registration.unregister().then(() => { - window.location.reload(); - }); - }); - } else { - // Service worker found. Proceed as normal. - registerValidSW(swUrl, config); - } - }) - .catch(() => { - console.log( - 'No internet connection found. App is running in offline mode.' - ); - }); -} - -export function unregister() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { - registration.unregister(); - }); - } -}