From a0e3ee8a6f02d9b27e7f9405780d32018606bfe9 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Thu, 24 Aug 2023 13:15:55 +0100 Subject: [PATCH] feat: add a landing page for the Flow Platform Add a landing page for the Flow Platform. The landing page is a linktree-styled static page made with Go, HTML and CSS. --- .gitattributes | 2 + .gitignore | 1 + .golangci.yaml | 5 +- Dockerfile | 7 ++ LICENSE | 21 +++++ go.mod | 5 ++ go.sum | 2 + links.go | 75 ++++++++++++++++++ magefiles/mage.go | 5 +- main.go | 140 ++++++++++++++++++++++++++++++++++ web/html/base.tmpl.html | 31 ++++++++ web/static/css/stylesheet.css | 33 ++++++++ web/static/gpg/public.asc | 3 + web/static/img/favicon.ico | 3 + 14 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 .gitattributes create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 go.mod create mode 100644 go.sum create mode 100644 links.go create mode 100644 web/html/base.tmpl.html create mode 100644 web/static/css/stylesheet.css create mode 100644 web/static/gpg/public.asc create mode 100644 web/static/img/favicon.ico diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..57b7444 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +web/static/gpg/public.asc filter=lfs diff=lfs merge=lfs -text +web/static/img/favicon.ico filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index e69de29..83f32f9 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +/landing diff --git a/.golangci.yaml b/.golangci.yaml index 8549273..70caf86 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -18,5 +18,8 @@ linters-settings: linters: enable-all: true - # disable: + disable: + - exhaustruct + - exhaustivestruct + - gomnd fast: false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b41f1b1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM scratch + +ADD landing /landing + +USER 65534 + +ENTRYPOINT ["/landing"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..21a9967 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Dan Anglin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a1fb0d6 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module codeflow.dananglin.me.uk/flow/landing + +go 1.21.0 + +require github.com/magefile/mage v1.15.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4ee1b87 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= diff --git a/links.go b/links.go new file mode 100644 index 0000000..309e259 --- /dev/null +++ b/links.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + "strings" +) + +type linkParseError struct { + msg string +} + +func (e linkParseError) Error() string { + return "link parse error: " + e.msg +} + +type link struct { + Title string + URL string + Rel string +} + +func (l link) String() string { + return fmt.Sprintf("Title: %s, URL: %s, Rel: %s", l.Title, l.URL, l.Rel) +} + +type links []link + +func (l *links) Set(value string) error { + linkAttributes := strings.Split(value, ",") + + lenLinkAttributes := len(linkAttributes) + if lenLinkAttributes != 2 && lenLinkAttributes != 3 { + return linkParseError{msg: fmt.Sprintf("unexpected number of attributes found %s, want 2 or 3, got %d", value, lenLinkAttributes)} + } + + var thisLink link + + for _, attr := range linkAttributes { + split := strings.Split(attr, "=") + switch strings.ToLower(split[0]) { + case "title": + thisLink.Title = split[1] + case "url": + thisLink.URL = split[1] + case "rel": + thisLink.Rel = split[1] + } + } + + if thisLink.Title == "" { + return linkParseError{msg: fmt.Sprintf("the title attribute is missing from %s", value)} + } + + if thisLink.URL == "" { + return linkParseError{msg: fmt.Sprintf("the URL attribute is missing from %s", value)} + } + + *l = append(*l, thisLink) + + return nil +} + +func (l *links) String() string { + if len(*l) == 0 { + return "" + } + + var builder strings.Builder + + for _, link := range *l { + fmt.Fprintln(&builder, link) + } + + return builder.String() +} diff --git a/magefiles/mage.go b/magefiles/mage.go index 1a12222..0097afd 100644 --- a/magefiles/mage.go +++ b/magefiles/mage.go @@ -11,7 +11,7 @@ import ( var Default = Build -var binary = "app" +var binary = "landing" // Test run the go tests. // To enable verbose mode set GO_TEST_VERBOSE=1. @@ -39,8 +39,7 @@ func Lint() error { // Build build the executable. func Build() error { - main := "main.go" - return sh.Run("go", "build", "-o", binary, main) + return sh.Run("go", "build", "-o", binary, ".") } // Clean clean the workspace. diff --git a/main.go b/main.go index da29a2c..f156e5b 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,144 @@ package main +import ( + "embed" + "flag" + "fmt" + "html/template" + "io/fs" + "log/slog" + "net/http" + "os" + "strings" + "time" +) + +//go:embed web/static/* +var staticFS embed.FS + +//go:embed web/html/* +var htmlTemplates embed.FS + func main() { + var ( + address string + profiles links + services links + ) + + flag.StringVar(&address, "address", "0.0.0.0:8080", "The address that the web server will listen on") + flag.Var(&services, "service", "A list of service links") + flag.Var(&profiles, "profile", "A list of profile links") + + flag.Parse() + + setupLogging() + + mux, err := routes(services, profiles) + if err != nil { + slog.Error("Unable to create the Mux", "error", err) + os.Exit(1) + } + + server := http.Server{ + Addr: address, + Handler: mux, + ReadHeaderTimeout: 1 * time.Second, + ReadTimeout: 5 * time.Second, + } + + slog.Info("Starting web application.", "address", address) + + err = server.ListenAndServe() + if err != nil { + slog.Error("Failed to run the web application", "error", err) + } +} + +func routes(services, profiles links) (*http.ServeMux, error) { + mux := http.NewServeMux() + + staticRootFS, err := fs.Sub(staticFS, "web/static") + if err != nil { + return nil, fmt.Errorf("unable to create the static root file system; %w", err) + } + + fileServer := http.FileServer(http.FS(staticRootFS)) + mux.Handle("/static/", http.StripPrefix("/static", neuter(fileServer))) + + mux.HandleFunc("/", landing(services, profiles)) + + return mux, nil +} + +func neuter(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if strings.HasSuffix(request.URL.Path, "/") { + notFound(writer) + + return + } + + next.ServeHTTP(writer, request) + }) +} + +func landing(services, profiles links) func(http.ResponseWriter, *http.Request) { + return func(writer http.ResponseWriter, request *http.Request) { + if request.URL.Path != "/" { + notFound(writer) + + return + } + + if request.Method != http.MethodGet { + writer.Header().Set("Allow", http.MethodGet) + clientError(writer, http.StatusMethodNotAllowed) + + return + } + + tmpl, err := template.New("base").ParseFS(htmlTemplates, "web/html/base.tmpl.html") + if err != nil { + serverError(writer, fmt.Errorf("error parsing the HTML template; %w", err)) + + return + } + + links := struct { + Services links + Profiles links + }{ + Services: services, + Profiles: profiles, + } + + if err = tmpl.Execute(writer, &links); err != nil { + serverError(writer, fmt.Errorf("error rendering the HTML templates; %w", err)) + } + } +} + +func notFound(w http.ResponseWriter) { + clientError(w, http.StatusNotFound) +} + +func clientError(w http.ResponseWriter, status int) { + http.Error(w, http.StatusText(status), status) +} + +func serverError(w http.ResponseWriter, err error) { + slog.Error(err.Error()) + + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) +} + +func setupLogging() { + opts := slog.HandlerOptions{ + AddSource: false, + } + + logger := slog.New(slog.NewJSONHandler(os.Stdout, &opts)) + + slog.SetDefault(logger) } diff --git a/web/html/base.tmpl.html b/web/html/base.tmpl.html new file mode 100644 index 0000000..172725e --- /dev/null +++ b/web/html/base.tmpl.html @@ -0,0 +1,31 @@ +{{ define "base" }} + + + + + Dan Anglin | Flow Platform + + + + + +

Flow Platform

+ {{ if gt (len .Services) 0 }} +

Services

+ + {{ end }} + {{ if gt (len .Profiles) 0 }} +

My Profiles

+ + {{ end }} + + +{{ end }} diff --git a/web/static/css/stylesheet.css b/web/static/css/stylesheet.css new file mode 100644 index 0000000..bbd6778 --- /dev/null +++ b/web/static/css/stylesheet.css @@ -0,0 +1,33 @@ +body { + background-color: #1e1c31; + text-align: center; + font-family: sans-serif; + font-weight: 600; +} + +h1, h2{ + color: white; +} + +body .links { + max-width: 500px; + margin: auto; + text-align: center; +} + +body .links p a { + display: block; + background-color: #296ae2; + color: Thistle; + text-decoration: none; + padding: 15px; + margin-bottom: 10px; + transition: 0.5s; + border-radius: 5px; +} + +body .links p a:hover, a:active { + background-color: #719dee; + color: White; + transition: 0.5s; +} diff --git a/web/static/gpg/public.asc b/web/static/gpg/public.asc new file mode 100644 index 0000000..e26f5ab --- /dev/null +++ b/web/static/gpg/public.asc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a3ceeed919889fa8e8d970370e36b9d59684205121df7d986b1dff2bad28912 +size 3618 diff --git a/web/static/img/favicon.ico b/web/static/img/favicon.ico new file mode 100644 index 0000000..6dc9d90 --- /dev/null +++ b/web/static/img/favicon.ico @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1738be5eb5b9fd0ea8ed0f396d987829d618d25de10c48dcc245653fc96a771b +size 1150