An OIDC-protected index page for your homeserver.

feat: push initial version to github

Graham Barber 724a2dbe

+1568
+3
.dockerignore
··· 1 + *.kdl 2 + .env 3 + Dockerfile
+21
.github/workflows/build.yml
··· 1 + name: Build binary 2 + 3 + on: 4 + push: 5 + pull_request: 6 + branches: [ $default-branch ] 7 + 8 + jobs: 9 + build: 10 + runs-on: ubuntu-latest 11 + steps: 12 + - name: Checkout repo 13 + uses: actions/checkout@v4 14 + 15 + - name: Set up Go 16 + uses: actions/setup-go@v4 17 + with: 18 + go-version: '1.23' 19 + 20 + - name: Build 21 + run: go build -v .
+53
.github/workflows/publish.yml
··· 1 + name: Build and publish container image 2 + 3 + on: 4 + release: 5 + types: 6 + - published 7 + 8 + env: 9 + REGISTRY: ghcr.io 10 + IMAGE_NAME: ${{ github.repository }} 11 + 12 + jobs: 13 + build-and-push-image: 14 + runs-on: ubuntu-latest 15 + 16 + permissions: 17 + contents: read 18 + packages: write 19 + attestations: write 20 + id-token: write 21 + 22 + steps: 23 + - name: Checkout repository 24 + uses: actions/checkout@v4 25 + 26 + - name: Authenticate with Container registry 27 + uses: docker/login-action@v3 28 + with: 29 + registry: ${{ env.REGISTRY }} 30 + username: ${{ github.actor }} 31 + password: ${{ secrets.GITHUB_TOKEN }} 32 + 33 + - name: Extract metadata for Docker 34 + id: meta 35 + uses: docker/metadata-action@v5 36 + with: 37 + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 38 + 39 + - name: Build and Push Docker image 40 + id: push 41 + uses: docker/build-push-action@v6 42 + with: 43 + context: . 44 + push: true 45 + tags: ${{ steps.meta.outputs.tags }} 46 + labels: ${{ steps.meta.outputs.labels }} 47 + 48 + - name: Generate artifact attestation 49 + uses: actions/attest-build-provenance@v2 50 + with: 51 + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 52 + subject-digest: ${{ steps.push.outputs.digest }} 53 + push-to-registry: true
+2
.gitignore
··· 1 + .env 2 + *.kdl
+19
Dockerfile
··· 1 + FROM golang:1.23 AS build 2 + 3 + WORKDIR /app 4 + 5 + COPY go.mod go.sum ./ 6 + RUN go mod download 7 + 8 + COPY . ./ 9 + 10 + RUN CGO_ENABLED=0 GOOS=linux go build -o /ladon . 11 + 12 + FROM alpine:3 AS run 13 + 14 + WORKDIR / 15 + 16 + COPY --from=build /ladon /ladon 17 + 18 + EXPOSE 4000 19 + ENTRYPOINT ["/ladon"]
+21
LICENSE.md
··· 1 + MIT License 2 + 3 + Copyright (c) 2024 Graham Barber 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+4
README.md
··· 1 + # Ladon 2 + 3 + A dead-simple index page for your homeserver, protected via OpenID Connect. 4 +
+151
auth/auth.go
··· 1 + package auth 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "fmt" 8 + "log/slog" 9 + "net/http" 10 + "time" 11 + 12 + gonanoid "github.com/matoous/go-nanoid/v2" 13 + "github.com/zitadel/logging" 14 + "github.com/zitadel/oidc/v3/pkg/client/rp" 15 + httphelper "github.com/zitadel/oidc/v3/pkg/http" 16 + "github.com/zitadel/oidc/v3/pkg/oidc" 17 + ) 18 + 19 + const SESSION_NAME = "_ladon_session" 20 + 21 + var ( 22 + ErrNoSession = errors.New("ladon: no session cookie set") 23 + ErrSessionExpired = errors.New("ladon: session expired") 24 + ) 25 + 26 + func State() string { 27 + return gonanoid.Must() 28 + } 29 + 30 + type AuthManager struct { 31 + Env *EnvConfig 32 + CookieHandler *httphelper.CookieHandler 33 + RelyingParty rp.RelyingParty 34 + Log *slog.Logger 35 + HttpClient *http.Client 36 + } 37 + 38 + func NewAuthManager(logger *slog.Logger) *AuthManager { 39 + env := EnvMustParse() 40 + 41 + cookieHandler := httphelper.NewCookieHandler(env.SessionSecret, env.SessionSecret) 42 + 43 + client := &http.Client{ 44 + Timeout: time.Minute, 45 + } 46 + 47 + options := []rp.Option{ 48 + rp.WithCookieHandler(cookieHandler), 49 + rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)), 50 + rp.WithHTTPClient(client), 51 + rp.WithLogger(logger), 52 + rp.WithSigningAlgsFromDiscovery(), 53 + } 54 + 55 + logging.EnableHTTPClient(client, 56 + logging.WithClientGroup("client"), 57 + ) 58 + 59 + ctx := logging.ToContext(context.TODO(), logger) 60 + provider, err := rp.NewRelyingPartyOIDC( 61 + ctx, 62 + env.Issuer, 63 + env.ClientID, 64 + env.ClientSecret, 65 + fmt.Sprintf("%s/callback", env.LadonHost), 66 + []string{"openid profile"}, 67 + options..., 68 + ) 69 + if err != nil { 70 + logger.Error("ladon: failed to instantiate relying party client") 71 + panic(err) 72 + } 73 + 74 + return &AuthManager{ 75 + Env: env, 76 + CookieHandler: cookieHandler, 77 + RelyingParty: provider, 78 + Log: logger, 79 + HttpClient: client, 80 + } 81 + } 82 + 83 + func (a *AuthManager) HandleLogin() http.Handler { 84 + return rp.AuthURLHandler( 85 + State, 86 + a.RelyingParty, 87 + ) 88 + } 89 + 90 + func (a *AuthManager) HandleCallback() http.Handler { 91 + return rp.CodeExchangeHandler( 92 + func( 93 + w http.ResponseWriter, 94 + r *http.Request, 95 + tokens *oidc.Tokens[*oidc.IDTokenClaims], 96 + state string, 97 + rp rp.RelyingParty, 98 + ) { 99 + data, err := json.Marshal(tokens) 100 + if err != nil { 101 + http.Error(w, err.Error(), http.StatusInternalServerError) 102 + return 103 + } 104 + 105 + a.CookieHandler.SetCookie(w, SESSION_NAME, string(data)) 106 + 107 + w.Header().Add("Location", "/") 108 + w.WriteHeader(http.StatusFound) 109 + }, 110 + a.RelyingParty, 111 + ) 112 + } 113 + 114 + func (a *AuthManager) HandleLogout() http.Handler { 115 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 116 + a.CookieHandler.DeleteCookie(w, SESSION_NAME) 117 + 118 + w.Header().Add("Location", "/") 119 + w.WriteHeader(http.StatusFound) 120 + }) 121 + } 122 + 123 + func (a *AuthManager) GetSession(r *http.Request) (*oidc.IDTokenClaims, error) { 124 + payload, err := a.CookieHandler.CheckCookie(r, SESSION_NAME) 125 + if errors.Is(err, http.ErrNoCookie) { 126 + return nil, ErrNoSession 127 + } else if err != nil { 128 + return nil, err 129 + } 130 + 131 + tokens := &oidc.Tokens[*oidc.IDTokenClaims]{} 132 + json.Unmarshal([]byte(payload), tokens) 133 + 134 + claims, err := rp.VerifyTokens[*oidc.IDTokenClaims]( 135 + context.TODO(), 136 + tokens.AccessToken, 137 + tokens.IDToken, 138 + a.RelyingParty.IDTokenVerifier(), 139 + ) 140 + if errors.Is(err, oidc.ErrExpired) { 141 + return nil, ErrSessionExpired 142 + } else if err != nil { 143 + return nil, err 144 + } 145 + 146 + return claims, nil 147 + } 148 + 149 + func (a *AuthManager) DeleteSession(w http.ResponseWriter) { 150 + a.CookieHandler.DeleteCookie(w, SESSION_NAME) 151 + }
+51
auth/env.go
··· 1 + package auth 2 + 3 + import ( 4 + "log" 5 + "os" 6 + 7 + gonanoid "github.com/matoous/go-nanoid/v2" 8 + ) 9 + 10 + type EnvConfig struct { 11 + LadonHost string 12 + ClientID string 13 + ClientSecret string 14 + Issuer string 15 + SessionSecret []byte 16 + } 17 + 18 + const LADON_HOST_ENV_KEY = "LADON_DOMAIN" 19 + const OIDC_ID_ENV_KEY = "OIDC_CLIENT_ID" 20 + const OIDC_SECRET_ENV_KEY = "OIDC_CLIENT_SECRET" 21 + const OIDC_ISSUER_ENV_KEY = "OIDC_ISSUER" 22 + const SESSION_SECRET = "SESSION_SECRET" 23 + 24 + func ensureEnvVar(key string) string { 25 + val, isSet := os.LookupEnv(key) 26 + 27 + if !isSet { 28 + log.Fatalf("%s is not set in environment", key) 29 + } 30 + 31 + return val 32 + } 33 + 34 + func EnvMustParse() *EnvConfig { 35 + sessionSecret := os.Getenv(SESSION_SECRET) 36 + 37 + if len(sessionSecret) != 16 { 38 + log.Fatalf("session secret must be 16 characters") 39 + } else if sessionSecret == "" { 40 + log.Println("ladon: no session secret set, generating one") 41 + sessionSecret = gonanoid.Must(16) 42 + } 43 + 44 + return &EnvConfig{ 45 + LadonHost: ensureEnvVar(LADON_HOST_ENV_KEY), 46 + ClientID: ensureEnvVar(OIDC_ID_ENV_KEY), 47 + ClientSecret: ensureEnvVar(OIDC_SECRET_ENV_KEY), 48 + Issuer: ensureEnvVar(OIDC_ISSUER_ENV_KEY), 49 + SessionSecret: []byte(sessionSecret), 50 + } 51 + }
+33
go.mod
··· 1 + module ladon 2 + 3 + go 1.23.6 4 + 5 + require ( 6 + github.com/a-h/templ v0.3.833 7 + github.com/sblinch/kdl-go v0.0.0-20240410000746-21754ba9ac55 8 + github.com/zitadel/oidc/v3 v3.34.1 9 + ) 10 + 11 + require ( 12 + github.com/go-logr/logr v1.4.2 // indirect 13 + github.com/go-logr/stdr v1.2.2 // indirect 14 + github.com/google/uuid v1.6.0 // indirect 15 + github.com/muhlemmer/gu v0.3.1 // indirect 16 + github.com/sirupsen/logrus v1.9.3 // indirect 17 + github.com/zitadel/schema v1.3.0 // indirect 18 + go.opentelemetry.io/otel v1.29.0 // indirect 19 + go.opentelemetry.io/otel/metric v1.29.0 // indirect 20 + go.opentelemetry.io/otel/trace v1.29.0 // indirect 21 + golang.org/x/sys v0.28.0 // indirect 22 + golang.org/x/text v0.21.0 // indirect 23 + ) 24 + 25 + require ( 26 + github.com/go-jose/go-jose/v4 v4.0.4 // indirect 27 + github.com/gorilla/securecookie v1.1.2 // indirect 28 + github.com/joho/godotenv v1.5.1 29 + github.com/matoous/go-nanoid/v2 v2.1.0 30 + github.com/zitadel/logging v0.6.1 31 + golang.org/x/crypto v0.31.0 // indirect 32 + golang.org/x/oauth2 v0.26.0 // indirect 33 + )
+75
go.sum
··· 1 + github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU= 2 + github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk= 3 + github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ= 4 + github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 5 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 + github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= 9 + github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 10 + github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= 11 + github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= 12 + github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 13 + github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 14 + github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 15 + github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 16 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 17 + github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 18 + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 19 + github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 20 + github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 21 + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 22 + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 23 + github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 24 + github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 25 + github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= 26 + github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= 27 + github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 28 + github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 29 + github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= 30 + github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= 31 + github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= 32 + github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= 33 + github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= 34 + github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= 35 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 + github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= 38 + github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 39 + github.com/sblinch/kdl-go v0.0.0-20240410000746-21754ba9ac55 h1:scyq0E9FvdGLX5lxAwjK0HebTM3Y7dG3tYrlXP+x+tk= 40 + github.com/sblinch/kdl-go v0.0.0-20240410000746-21754ba9ac55/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28= 41 + github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 42 + github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 43 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 44 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 45 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 46 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 47 + github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y= 48 + github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= 49 + github.com/zitadel/oidc/v3 v3.34.1 h1:/rxx2HxEowd8Sdb8sxcRxTu9pLy3/TXBLrewKOUMTHA= 50 + github.com/zitadel/oidc/v3 v3.34.1/go.mod h1:lhAdAP1iWAnpfWF8CWNiO6yKvGFtPMuAubPwP5JC7Ec= 51 + github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= 52 + github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= 53 + go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 54 + go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 55 + go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 56 + go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 57 + go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 58 + go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 59 + golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 60 + golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 61 + golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 62 + golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 63 + golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= 64 + golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 65 + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 + golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 67 + golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 68 + golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 69 + golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 70 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 71 + gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 72 + gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 73 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 74 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 75 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+93
main.go
··· 1 + package main 2 + 3 + import ( 4 + "embed" 5 + "errors" 6 + "log" 7 + "log/slog" 8 + "net/http" 9 + "os" 10 + 11 + _ "github.com/joho/godotenv/autoload" 12 + gonanoid "github.com/matoous/go-nanoid/v2" 13 + "github.com/sblinch/kdl-go" 14 + "github.com/zitadel/logging" 15 + 16 + "github.com/a-h/templ" 17 + 18 + "ladon/auth" 19 + "ladon/views" 20 + ) 21 + 22 + //go:embed static 23 + var content embed.FS 24 + 25 + func ServeRoot(am *auth.AuthManager) http.Handler { 26 + f, err := os.Open("./data/links.kdl") 27 + if err != nil { 28 + am.Log.Error("ladon: failed to open KDL config") 29 + panic(err) 30 + } 31 + 32 + doc, err := kdl.Parse(f) 33 + if err != nil { 34 + am.Log.Error("ladon: failed to pase KDL config") 35 + panic(err) 36 + } 37 + 38 + return http.HandlerFunc( 39 + func(w http.ResponseWriter, r *http.Request) { 40 + claims, err := am.GetSession(r) 41 + 42 + if errors.Is(err, auth.ErrNoSession) { 43 + templ.Handler(views.Authenticate()).ServeHTTP(w, r) 44 + return 45 + } else if errors.Is(err, auth.ErrSessionExpired) { 46 + am.DeleteSession(w) 47 + am.HandleLogin().ServeHTTP(w, r) 48 + return 49 + } else if err != nil { 50 + http.Error(w, err.Error(), http.StatusInternalServerError) 51 + return 52 + } 53 + 54 + templ.Handler(views.Links(claims.PreferredUsername, doc)).ServeHTTP(w, r) 55 + }, 56 + ) 57 + } 58 + 59 + func main() { 60 + logger := slog.New( 61 + slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ 62 + AddSource: true, 63 + Level: slog.LevelDebug, 64 + }), 65 + ) 66 + 67 + am := auth.NewAuthManager(logger) 68 + 69 + // Handle static assets 70 + fs := http.FileServer(http.FS(content)) 71 + http.Handle("/static/", fs) 72 + 73 + // Serve pages 74 + http.Handle("/", ServeRoot(am)) 75 + 76 + // Handle authentication 77 + http.Handle("/login", am.HandleLogin()) 78 + http.Handle("/logout", am.HandleLogout()) 79 + http.Handle("/callback", am.HandleCallback()) 80 + 81 + mw := logging.Middleware( 82 + logging.WithLogger(logger), 83 + logging.WithGroup("server"), 84 + logging.WithIDFunc(func() slog.Attr { 85 + return slog.String("id", gonanoid.Must()) 86 + }), 87 + ) 88 + 89 + log.Println("Listening on port 4000") 90 + if err := http.ListenAndServe(":4000", mw(http.DefaultServeMux)); err != nil { 91 + panic(err) 92 + } 93 + }
+1
static/favicon.svg
··· 1 + <svg width="500" xmlns="http://www.w3.org/2000/svg" height="500" id="screenshot-2b26997c-de13-802e-8005-aeab33af5504" viewBox="0 0 500 500" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1"><g id="shape-2b26997c-de13-802e-8005-aeab33af5504" data-testid="Favicon"><defs><clipPath id="frame-clip-2b26997c-de13-802e-8005-aeab33af5504-render-1" class="frame-clip frame-clip-def"><rect rx="0" ry="0" x="0" y="0" width="500" height="500" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)"/></clipPath></defs><g clip-path="url(#frame-clip-2b26997c-de13-802e-8005-aeab33af5504-render-1)" fill="none"><g class="fills" id="fills-2b26997c-de13-802e-8005-aeab33af5504"><rect rx="0" ry="0" x="0" y="0" width="500" height="500" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" class="frame-background"/></g><g class="frame-children"><g id="shape-dfe65906-e08a-8092-8005-b7da115b0253" data-testid="Rectangle"><g class="fills" id="fills-dfe65906-e08a-8092-8005-b7da115b0253"><rect rx="150" ry="150" x="0" y="0" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" width="500" height="500" style="fill: rgb(31, 29, 46); fill-opacity: 1;"/></g></g><g id="shape-dfe65906-e08a-8092-8005-b7da3723682d" data-testid="Ellipse"><g class="fills" id="fills-dfe65906-e08a-8092-8005-b7da3723682d"><ellipse cx="250" cy="250" rx="100" ry="100" transform="matrix(1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000)" style="fill: rgb(246, 193, 119); fill-opacity: 1;"/></g></g></g></g></g></svg>
+580
static/styles.css
··· 1 + /*! tailwindcss v4.0.6 | MIT License | https://tailwindcss.com */ 2 + @layer theme, base, components, utilities; 3 + @layer theme { 4 + :root, :host { 5 + --font-sans: "Recursive", sans-serif; 6 + --font-serif: "Recursive", serif; 7 + --font-mono: "Recursive", monospace; 8 + --spacing: 0.25rem; 9 + --breakpoint-sm: 40rem; 10 + --breakpoint-md: 48rem; 11 + --breakpoint-lg: 64rem; 12 + --breakpoint-xl: 80rem; 13 + --breakpoint-2xl: 96rem; 14 + --container-3xs: 16rem; 15 + --container-2xs: 18rem; 16 + --container-xs: 20rem; 17 + --container-sm: 24rem; 18 + --container-md: 28rem; 19 + --container-lg: 32rem; 20 + --container-xl: 36rem; 21 + --container-2xl: 42rem; 22 + --container-3xl: 48rem; 23 + --container-4xl: 56rem; 24 + --container-5xl: 64rem; 25 + --container-6xl: 72rem; 26 + --container-7xl: 80rem; 27 + --text-xs: 0.75rem; 28 + --text-xs--line-height: calc(1 / 0.75); 29 + --text-sm: 0.875rem; 30 + --text-sm--line-height: calc(1.25 / 0.875); 31 + --text-base: 1rem; 32 + --text-base--line-height: calc(1.5 / 1); 33 + --text-lg: 1.125rem; 34 + --text-lg--line-height: calc(1.75 / 1.125); 35 + --text-xl: 1.25rem; 36 + --text-xl--line-height: calc(1.75 / 1.25); 37 + --text-2xl: 1.5rem; 38 + --text-2xl--line-height: calc(2 / 1.5); 39 + --text-3xl: 1.875rem; 40 + --text-3xl--line-height: calc(2.25 / 1.875); 41 + --text-4xl: 2.25rem; 42 + --text-4xl--line-height: calc(2.5 / 2.25); 43 + --text-5xl: 3rem; 44 + --text-5xl--line-height: 1; 45 + --text-6xl: 3.75rem; 46 + --text-6xl--line-height: 1; 47 + --text-7xl: 4.5rem; 48 + --text-7xl--line-height: 1; 49 + --text-8xl: 6rem; 50 + --text-8xl--line-height: 1; 51 + --text-9xl: 8rem; 52 + --text-9xl--line-height: 1; 53 + --font-weight-thin: 100; 54 + --font-weight-extralight: 200; 55 + --font-weight-light: 300; 56 + --font-weight-normal: 400; 57 + --font-weight-medium: 500; 58 + --font-weight-semibold: 600; 59 + --font-weight-bold: 700; 60 + --font-weight-extrabold: 800; 61 + --font-weight-black: 900; 62 + --tracking-tighter: -0.05em; 63 + --tracking-tight: -0.025em; 64 + --tracking-normal: 0em; 65 + --tracking-wide: 0.025em; 66 + --tracking-wider: 0.05em; 67 + --tracking-widest: 0.1em; 68 + --leading-tight: 1.25; 69 + --leading-snug: 1.375; 70 + --leading-normal: 1.5; 71 + --leading-relaxed: 1.625; 72 + --leading-loose: 2; 73 + --radius-xs: 0.125rem; 74 + --radius-sm: 0.25rem; 75 + --radius-md: 0.375rem; 76 + --radius-lg: 0.5rem; 77 + --radius-xl: 0.75rem; 78 + --radius-2xl: 1rem; 79 + --radius-3xl: 1.5rem; 80 + --radius-4xl: 2rem; 81 + --shadow-2xs: 0 1px rgb(0 0 0 / 0.05); 82 + --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); 83 + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); 84 + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); 85 + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); 86 + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); 87 + --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); 88 + --inset-shadow-2xs: inset 0 1px rgb(0 0 0 / 0.05); 89 + --inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05); 90 + --inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05); 91 + --drop-shadow-xs: 0 1px 1px rgb(0 0 0 / 0.05); 92 + --drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15); 93 + --drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12); 94 + --drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15); 95 + --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1); 96 + --drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15); 97 + --ease-in: cubic-bezier(0.4, 0, 1, 1); 98 + --ease-out: cubic-bezier(0, 0, 0.2, 1); 99 + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); 100 + --animate-spin: spin 1s linear infinite; 101 + --animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; 102 + --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 103 + --animate-bounce: bounce 1s infinite; 104 + --blur-xs: 4px; 105 + --blur-sm: 8px; 106 + --blur-md: 12px; 107 + --blur-lg: 16px; 108 + --blur-xl: 24px; 109 + --blur-2xl: 40px; 110 + --blur-3xl: 64px; 111 + --perspective-dramatic: 100px; 112 + --perspective-near: 300px; 113 + --perspective-normal: 500px; 114 + --perspective-midrange: 800px; 115 + --perspective-distant: 1200px; 116 + --aspect-video: 16 / 9; 117 + --default-transition-duration: 150ms; 118 + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 119 + --default-font-family: var(--font-sans); 120 + --default-font-feature-settings: var(--font-sans--font-feature-settings); 121 + --default-font-variation-settings: var(--font-sans--font-variation-settings); 122 + --default-mono-font-family: var(--font-mono); 123 + --default-mono-font-feature-settings: var(--font-mono--font-feature-settings); 124 + --default-mono-font-variation-settings: var(--font-mono--font-variation-settings); 125 + --color-base: rgb(25, 23, 36); 126 + --color-surface: rgb(31, 29, 46); 127 + --color-overlay: rgb(38, 35, 58); 128 + --color-muted: rgb(110, 106, 134); 129 + --color-subtle: rgb(144, 140, 170); 130 + --color-text: rgb(224, 222, 244); 131 + --color-love: rgb(235, 111, 146); 132 + --color-gold: rgb(246, 193, 119); 133 + --color-rose: rgb(235, 188, 186); 134 + --color-pine: rgb(49, 116, 143); 135 + --color-foam: rgb(156, 207, 216); 136 + --color-iris: rgb(196, 167, 231); 137 + --color-highlight-low: rgb(33, 32, 46); 138 + --color-highlight-med: rgb(64, 61, 82); 139 + --color-highlight-high: rgb(82, 79, 103); 140 + --font-serif--font-variation-settings: "slnt" -12, "CASL" 1, "CRSV" 1; 141 + --font-mono--font-variation-settings: "MONO" 1; 142 + } 143 + } 144 + @layer base { 145 + *, ::after, ::before, ::backdrop, ::file-selector-button { 146 + box-sizing: border-box; 147 + margin: 0; 148 + padding: 0; 149 + border: 0 solid; 150 + } 151 + html, :host { 152 + line-height: 1.5; 153 + -webkit-text-size-adjust: 100%; 154 + tab-size: 4; 155 + font-family: var( --default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' ); 156 + font-feature-settings: var(--default-font-feature-settings, normal); 157 + font-variation-settings: var(--default-font-variation-settings, normal); 158 + -webkit-tap-highlight-color: transparent; 159 + } 160 + body { 161 + line-height: inherit; 162 + } 163 + hr { 164 + height: 0; 165 + color: inherit; 166 + border-top-width: 1px; 167 + } 168 + abbr:where([title]) { 169 + -webkit-text-decoration: underline dotted; 170 + text-decoration: underline dotted; 171 + } 172 + h1, h2, h3, h4, h5, h6 { 173 + font-size: inherit; 174 + font-weight: inherit; 175 + } 176 + a { 177 + color: inherit; 178 + -webkit-text-decoration: inherit; 179 + text-decoration: inherit; 180 + } 181 + b, strong { 182 + font-weight: bolder; 183 + } 184 + code, kbd, samp, pre { 185 + font-family: var( --default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace ); 186 + font-feature-settings: var(--default-mono-font-feature-settings, normal); 187 + font-variation-settings: var(--default-mono-font-variation-settings, normal); 188 + font-size: 1em; 189 + } 190 + small { 191 + font-size: 80%; 192 + } 193 + sub, sup { 194 + font-size: 75%; 195 + line-height: 0; 196 + position: relative; 197 + vertical-align: baseline; 198 + } 199 + sub { 200 + bottom: -0.25em; 201 + } 202 + sup { 203 + top: -0.5em; 204 + } 205 + table { 206 + text-indent: 0; 207 + border-color: inherit; 208 + border-collapse: collapse; 209 + } 210 + :-moz-focusring { 211 + outline: auto; 212 + } 213 + progress { 214 + vertical-align: baseline; 215 + } 216 + summary { 217 + display: list-item; 218 + } 219 + ol, ul, menu { 220 + list-style: none; 221 + } 222 + img, svg, video, canvas, audio, iframe, embed, object { 223 + display: block; 224 + vertical-align: middle; 225 + } 226 + img, video { 227 + max-width: 100%; 228 + height: auto; 229 + } 230 + button, input, select, optgroup, textarea, ::file-selector-button { 231 + font: inherit; 232 + font-feature-settings: inherit; 233 + font-variation-settings: inherit; 234 + letter-spacing: inherit; 235 + color: inherit; 236 + border-radius: 0; 237 + background-color: transparent; 238 + opacity: 1; 239 + } 240 + :where(select:is([multiple], [size])) optgroup { 241 + font-weight: bolder; 242 + } 243 + :where(select:is([multiple], [size])) optgroup option { 244 + padding-inline-start: 20px; 245 + } 246 + ::file-selector-button { 247 + margin-inline-end: 4px; 248 + } 249 + ::placeholder { 250 + opacity: 1; 251 + color: color-mix(in oklab, currentColor 50%, transparent); 252 + } 253 + textarea { 254 + resize: vertical; 255 + } 256 + ::-webkit-search-decoration { 257 + -webkit-appearance: none; 258 + } 259 + ::-webkit-date-and-time-value { 260 + min-height: 1lh; 261 + text-align: inherit; 262 + } 263 + ::-webkit-datetime-edit { 264 + display: inline-flex; 265 + } 266 + ::-webkit-datetime-edit-fields-wrapper { 267 + padding: 0; 268 + } 269 + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { 270 + padding-block: 0; 271 + } 272 + :-moz-ui-invalid { 273 + box-shadow: none; 274 + } 275 + button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button { 276 + appearance: button; 277 + } 278 + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { 279 + height: auto; 280 + } 281 + [hidden]:where(:not([hidden='until-found'])) { 282 + display: none !important; 283 + } 284 + } 285 + @layer utilities { 286 + .collapse { 287 + visibility: collapse; 288 + } 289 + .relative { 290 + position: relative; 291 + } 292 + .static { 293 + position: static; 294 + } 295 + .col-span-1 { 296 + grid-column: span 1 / span 1; 297 + } 298 + .col-span-full { 299 + grid-column: 1 / -1; 300 + } 301 + .mx-auto { 302 + margin-inline: auto; 303 + } 304 + .mb-3 { 305 + margin-bottom: calc(var(--spacing) * 3); 306 + } 307 + .mb-4 { 308 + margin-bottom: calc(var(--spacing) * 4); 309 + } 310 + .block { 311 + display: block; 312 + } 313 + .flex { 314 + display: flex; 315 + } 316 + .grid { 317 + display: grid; 318 + } 319 + .inline-flex { 320 + display: inline-flex; 321 + } 322 + .list-item { 323 + display: list-item; 324 + } 325 + .table { 326 + display: table; 327 + } 328 + .aspect-square { 329 + aspect-ratio: 1 / 1; 330 + } 331 + .h-4 { 332 + height: calc(var(--spacing) * 4); 333 + } 334 + .h-screen { 335 + height: 100vh; 336 + } 337 + .w-4 { 338 + width: calc(var(--spacing) * 4); 339 + } 340 + .w-full { 341 + width: 100%; 342 + } 343 + .w-screen { 344 + width: 100vw; 345 + } 346 + .max-w-xs { 347 + max-width: var(--container-xs); 348 + } 349 + .border-collapse { 350 + border-collapse: collapse; 351 + } 352 + .transform { 353 + transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y); 354 + } 355 + .resize { 356 + resize: both; 357 + } 358 + .grid-cols-3 { 359 + grid-template-columns: repeat(3, minmax(0, 1fr)); 360 + } 361 + .grid-cols-subgrid { 362 + grid-template-columns: subgrid; 363 + } 364 + .flex-col { 365 + flex-direction: column; 366 + } 367 + .items-center { 368 + align-items: center; 369 + } 370 + .justify-between { 371 + justify-content: space-between; 372 + } 373 + .justify-center { 374 + justify-content: center; 375 + } 376 + .gap-4 { 377 + gap: calc(var(--spacing) * 4); 378 + } 379 + .gap-x-4 { 380 + column-gap: calc(var(--spacing) * 4); 381 + } 382 + .gap-y-8 { 383 + row-gap: calc(var(--spacing) * 8); 384 + } 385 + .rounded { 386 + border-radius: 0.25rem; 387 + } 388 + .rounded-full { 389 + border-radius: calc(infinity * 1px); 390 + } 391 + .rounded-lg { 392 + border-radius: var(--radius-lg); 393 + } 394 + .border { 395 + border-style: var(--tw-border-style); 396 + border-width: 1px; 397 + } 398 + .bg-base { 399 + background-color: var(--color-base); 400 + } 401 + .bg-foam { 402 + background-color: var(--color-foam); 403 + } 404 + .bg-foam\/20 { 405 + background-color: color-mix(in oklab, var(--color-foam) 20%, transparent); 406 + } 407 + .bg-surface { 408 + background-color: var(--color-surface); 409 + } 410 + .p-4 { 411 + padding: calc(var(--spacing) * 4); 412 + } 413 + .px-2 { 414 + padding-inline: calc(var(--spacing) * 2); 415 + } 416 + .px-3 { 417 + padding-inline: calc(var(--spacing) * 3); 418 + } 419 + .py-2 { 420 + padding-block: calc(var(--spacing) * 2); 421 + } 422 + .pt-4 { 423 + padding-top: calc(var(--spacing) * 4); 424 + } 425 + .text-center { 426 + text-align: center; 427 + } 428 + .font-mono { 429 + font-family: var(--font-mono); 430 + font-variation-settings: var(--font-mono--font-variation-settings); 431 + } 432 + .font-serif { 433 + font-family: var(--font-serif); 434 + font-variation-settings: var(--font-serif--font-variation-settings); 435 + } 436 + .text-lg { 437 + font-size: var(--text-lg); 438 + line-height: var(--tw-leading, var(--text-lg--line-height)); 439 + } 440 + .text-sm { 441 + font-size: var(--text-sm); 442 + line-height: var(--tw-leading, var(--text-sm--line-height)); 443 + } 444 + .text-xs { 445 + font-size: var(--text-xs); 446 + line-height: var(--tw-leading, var(--text-xs--line-height)); 447 + } 448 + .leading-none { 449 + --tw-leading: 1; 450 + line-height: 1; 451 + } 452 + .font-bold { 453 + --tw-font-weight: var(--font-weight-bold); 454 + font-weight: var(--font-weight-bold); 455 + } 456 + .text-foam { 457 + color: var(--color-foam); 458 + } 459 + .text-gold { 460 + color: var(--color-gold); 461 + } 462 + .text-highlight-med { 463 + color: var(--color-highlight-med); 464 + } 465 + .text-iris { 466 + color: var(--color-iris); 467 + } 468 + .text-subtle { 469 + color: var(--color-subtle); 470 + } 471 + .text-text { 472 + color: var(--color-text); 473 + } 474 + .underline { 475 + text-decoration-line: underline; 476 + } 477 + .outline { 478 + outline-style: var(--tw-outline-style); 479 + outline-width: 1px; 480 + } 481 + .sm\:max-w-screen-sm { 482 + @media (width >= 40rem) { 483 + max-width: var(--breakpoint-sm); 484 + } 485 + } 486 + .sm\:grid-cols-4 { 487 + @media (width >= 40rem) { 488 + grid-template-columns: repeat(4, minmax(0, 1fr)); 489 + } 490 + } 491 + .md\:max-w-screen-md { 492 + @media (width >= 48rem) { 493 + max-width: var(--breakpoint-md); 494 + } 495 + } 496 + .md\:grid-cols-6 { 497 + @media (width >= 48rem) { 498 + grid-template-columns: repeat(6, minmax(0, 1fr)); 499 + } 500 + } 501 + .lg\:max-w-screen-lg { 502 + @media (width >= 64rem) { 503 + max-width: var(--breakpoint-lg); 504 + } 505 + } 506 + .lg\:grid-cols-8 { 507 + @media (width >= 64rem) { 508 + grid-template-columns: repeat(8, minmax(0, 1fr)); 509 + } 510 + } 511 + } 512 + @keyframes spin { 513 + to { 514 + transform: rotate(360deg); 515 + } 516 + } 517 + @keyframes ping { 518 + 75%, 100% { 519 + transform: scale(2); 520 + opacity: 0; 521 + } 522 + } 523 + @keyframes pulse { 524 + 50% { 525 + opacity: 0.5; 526 + } 527 + } 528 + @keyframes bounce { 529 + 0%, 100% { 530 + transform: translateY(-25%); 531 + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); 532 + } 533 + 50% { 534 + transform: none; 535 + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); 536 + } 537 + } 538 + @property --tw-rotate-x { 539 + syntax: "*"; 540 + inherits: false; 541 + initial-value: rotateX(0); 542 + } 543 + @property --tw-rotate-y { 544 + syntax: "*"; 545 + inherits: false; 546 + initial-value: rotateY(0); 547 + } 548 + @property --tw-rotate-z { 549 + syntax: "*"; 550 + inherits: false; 551 + initial-value: rotateZ(0); 552 + } 553 + @property --tw-skew-x { 554 + syntax: "*"; 555 + inherits: false; 556 + initial-value: skewX(0); 557 + } 558 + @property --tw-skew-y { 559 + syntax: "*"; 560 + inherits: false; 561 + initial-value: skewY(0); 562 + } 563 + @property --tw-border-style { 564 + syntax: "*"; 565 + inherits: false; 566 + initial-value: solid; 567 + } 568 + @property --tw-leading { 569 + syntax: "*"; 570 + inherits: false; 571 + } 572 + @property --tw-font-weight { 573 + syntax: "*"; 574 + inherits: false; 575 + } 576 + @property --tw-outline-style { 577 + syntax: "*"; 578 + inherits: false; 579 + initial-value: solid; 580 + }
+30
tailwind.css
··· 1 + @import "tailwindcss"; 2 + 3 + @theme { 4 + /* Rose Pine theme colors */ 5 + --color-*: initial; 6 + --color-base: rgb(25, 23, 36); 7 + --color-surface: rgb(31, 29, 46); 8 + --color-overlay: rgb(38, 35, 58); 9 + --color-muted: rgb(110, 106, 134); 10 + --color-subtle: rgb(144, 140, 170); 11 + --color-text: rgb(224, 222, 244); 12 + --color-love: rgb(235, 111, 146); 13 + --color-gold: rgb(246, 193, 119); 14 + --color-rose: rgb(235, 188, 186); 15 + --color-pine: rgb(49, 116, 143); 16 + --color-foam: rgb(156, 207, 216); 17 + --color-iris: rgb(196, 167, 231); 18 + --color-highlight-low: rgb(33, 32, 46); 19 + --color-highlight-med: rgb(64, 61, 82); 20 + --color-highlight-high: rgb(82, 79, 103); 21 + 22 + /* Recursive font settings */ 23 + --font-sans: "Recursive", sans-serif; 24 + 25 + --font-serif: "Recursive", serif; 26 + --font-serif--font-variation-settings: "slnt" -12, "CASL" 1, "CRSV" 1; 27 + 28 + --font-mono: "Recursive", monospace; 29 + --font-mono--font-variation-settings: "MONO" 1; 30 + }
+16
views/document.templ
··· 1 + package views 2 + 3 + templ Document(title string) { 4 + <html> 5 + <head> 6 + <title>{title} | Ladon</title> 7 + <link rel="icon" type="image/svg" href="/static/favicon.svg"/> 8 + <link rel="stylesheet" href="/static/styles.css"/> 9 + <link rel="preconnect" href="https://fonts.googleapis.com"> 10 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 11 + <link href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap" rel="stylesheet"> </head> 12 + <body class="bg-base text-text"> 13 + {children...} 14 + </body> 15 + </html> 16 + }
+61
views/document_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.3.833 4 + package views 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + func Document(title string) templ.Component { 12 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 13 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 14 + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { 15 + return templ_7745c5c3_CtxErr 16 + } 17 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 18 + if !templ_7745c5c3_IsBuffer { 19 + defer func() { 20 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 21 + if templ_7745c5c3_Err == nil { 22 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 23 + } 24 + }() 25 + } 26 + ctx = templ.InitializeContext(ctx) 27 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 28 + if templ_7745c5c3_Var1 == nil { 29 + templ_7745c5c3_Var1 = templ.NopComponent 30 + } 31 + ctx = templ.ClearChildren(ctx) 32 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<html><head><title>") 33 + if templ_7745c5c3_Err != nil { 34 + return templ_7745c5c3_Err 35 + } 36 + var templ_7745c5c3_Var2 string 37 + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) 38 + if templ_7745c5c3_Err != nil { 39 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/document.templ`, Line: 6, Col: 19} 40 + } 41 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 42 + if templ_7745c5c3_Err != nil { 43 + return templ_7745c5c3_Err 44 + } 45 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " | Ladon</title><link rel=\"icon\" type=\"image/svg\" href=\"/static/favicon.svg\"><link rel=\"stylesheet\" href=\"/static/styles.css\"><link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin><link href=\"https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&amp;display=swap\" rel=\"stylesheet\"></head><body class=\"bg-base text-text\">") 46 + if templ_7745c5c3_Err != nil { 47 + return templ_7745c5c3_Err 48 + } 49 + templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer) 50 + if templ_7745c5c3_Err != nil { 51 + return templ_7745c5c3_Err 52 + } 53 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</body></html>") 54 + if templ_7745c5c3_Err != nil { 55 + return templ_7745c5c3_Err 56 + } 57 + return nil 58 + }) 59 + } 60 + 61 + var _ = templruntime.GeneratedTemplate
+79
views/index.templ
··· 1 + package views 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/sblinch/kdl-go/document" 7 + ) 8 + 9 + func StringToVibrantHSL(s string) string { 10 + var sum int 11 + for _, char := range s { 12 + sum += int(char) 13 + } 14 + hue := sum % 360 15 + return fmt.Sprintf("hsl(%d, 80%%, 50%%)", hue) 16 + } 17 + 18 + templ renderLink(name string, url string) { 19 + <li class="col-span-1 grid grid-cols-subgrid"> 20 + <a 21 + class="aspect-square bg-surface p-4 rounded-lg text-sm flex flex-col justify-between leading-none" 22 + href={ templ.URL(url) } 23 + target="_blank" 24 + rele="noopener noreferrer" 25 + > 26 + <div class="flex items-center justify-between"> 27 + <div class="w-4 h-4 rounded-full" style={fmt.Sprintf("background: %s", StringToVibrantHSL(name))}></div> 28 + <svg class="text-highlight-med" width="16" height="16" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 2C2.44772 2 2 2.44772 2 3V12C2 12.5523 2.44772 13 3 13H12C12.5523 13 13 12.5523 13 12V8.5C13 8.22386 12.7761 8 12.5 8C12.2239 8 12 8.22386 12 8.5V12H3V3L6.5 3C6.77614 3 7 2.77614 7 2.5C7 2.22386 6.77614 2 6.5 2H3ZM12.8536 2.14645C12.9015 2.19439 12.9377 2.24964 12.9621 2.30861C12.9861 2.36669 12.9996 2.4303 13 2.497L13 2.5V2.50049V5.5C13 5.77614 12.7761 6 12.5 6C12.2239 6 12 5.77614 12 5.5V3.70711L6.85355 8.85355C6.65829 9.04882 6.34171 9.04882 6.14645 8.85355C5.95118 8.65829 5.95118 8.34171 6.14645 8.14645L11.2929 3H9.5C9.22386 3 9 2.77614 9 2.5C9 2.22386 9.22386 2 9.5 2H12.4999H12.5C12.5678 2 12.6324 2.01349 12.6914 2.03794C12.7504 2.06234 12.8056 2.09851 12.8536 2.14645Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg> 29 + </div> 30 + <span class="block">{ name }</span> 31 + </a> 32 + </li> 33 + } 34 + 35 + templ renderGroup(name string, nodes []*document.Node) { 36 + <li class="col-span-full grid grid-cols-subgrid gap-4"> 37 + <h2 class="text-lg font-bold col-span-full pt-4">{ name }</h2> 38 + <ul class="col-span-full grid grid-cols-subgrid gap-4"> 39 + for _, node := range nodes { 40 + if node.Name.ValueString() == "group" { 41 + @renderGroup(node.Arguments[0].ValueString(), node.Children) 42 + } else if node.Name.String() == "link" { 43 + {{ url, _ := node.Properties.Get("url") }} 44 + @renderLink(node.Arguments[0].ValueString(), url.ValueString()) 45 + } 46 + } 47 + </ul> 48 + </li> 49 + } 50 + 51 + templ Authenticate() { 52 + @Document("Log In") { 53 + <div class="w-screen h-screen flex items-center justify-center"> 54 + <main class="w-full max-w-xs bg-surface rounded-lg p-4"> 55 + <p class="mb-3 font-serif">Can I see some ID, please?</p> 56 + <a href="/login" class="block text-center text-sm rounded py-2 px-2 bg-foam/20 text-foam">Log In with OIDC</a> 57 + </main> 58 + </div> 59 + } 60 + } 61 + 62 + templ Links(username string, doc *document.Document) { 63 + @Document("Links") { 64 + <div class="flex items-center justify-between px-3 py-2 text-subtle text-xs mb-4"> 65 + <div>Howdy, <span class="font-mono text-gold">{ username }</span></div> 66 + <a href="/logout" class="underline text-iris">Log Out</a> 67 + </div> 68 + <ul class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 px-3 gap-x-4 gap-y-8 max-w-screen-xs sm:max-w-screen-sm md:max-w-screen-md lg:max-w-screen-lg mx-auto"> 69 + for _, node := range doc.Nodes { 70 + if node.Name.ValueString() == "group" { 71 + @renderGroup(node.Arguments[0].ValueString(), node.Children) 72 + } else if node.Name.ValueString() == "link" { 73 + {{ url, _ := node.Properties.Get("url") }} 74 + @renderLink(node.Arguments[0].ValueString(), url.ValueString()) 75 + } 76 + } 77 + </ul> 78 + } 79 + }
+275
views/index_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.3.833 4 + package views 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import ( 12 + "fmt" 13 + 14 + "github.com/sblinch/kdl-go/document" 15 + ) 16 + 17 + func StringToVibrantHSL(s string) string { 18 + var sum int 19 + for _, char := range s { 20 + sum += int(char) 21 + } 22 + hue := sum % 360 23 + return fmt.Sprintf("hsl(%d, 80%%, 50%%)", hue) 24 + } 25 + 26 + func renderLink(name string, url string) templ.Component { 27 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 28 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 29 + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { 30 + return templ_7745c5c3_CtxErr 31 + } 32 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 33 + if !templ_7745c5c3_IsBuffer { 34 + defer func() { 35 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 36 + if templ_7745c5c3_Err == nil { 37 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 38 + } 39 + }() 40 + } 41 + ctx = templ.InitializeContext(ctx) 42 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 43 + if templ_7745c5c3_Var1 == nil { 44 + templ_7745c5c3_Var1 = templ.NopComponent 45 + } 46 + ctx = templ.ClearChildren(ctx) 47 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<li class=\"col-span-1 grid grid-cols-subgrid\"><a class=\"aspect-square bg-surface p-4 rounded-lg text-sm flex flex-col justify-between leading-none\" href=\"") 48 + if templ_7745c5c3_Err != nil { 49 + return templ_7745c5c3_Err 50 + } 51 + var templ_7745c5c3_Var2 templ.SafeURL = templ.URL(url) 52 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2))) 53 + if templ_7745c5c3_Err != nil { 54 + return templ_7745c5c3_Err 55 + } 56 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" target=\"_blank\" rele=\"noopener noreferrer\"><div class=\"flex items-center justify-between\"><div class=\"w-4 h-4 rounded-full\" style=\"") 57 + if templ_7745c5c3_Err != nil { 58 + return templ_7745c5c3_Err 59 + } 60 + var templ_7745c5c3_Var3 string 61 + templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background: %s", StringToVibrantHSL(name))) 62 + if templ_7745c5c3_Err != nil { 63 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 27, Col: 100} 64 + } 65 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 66 + if templ_7745c5c3_Err != nil { 67 + return templ_7745c5c3_Err 68 + } 69 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"></div><svg class=\"text-highlight-med\" width=\"16\" height=\"16\" viewBox=\"0 0 15 15\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M3 2C2.44772 2 2 2.44772 2 3V12C2 12.5523 2.44772 13 3 13H12C12.5523 13 13 12.5523 13 12V8.5C13 8.22386 12.7761 8 12.5 8C12.2239 8 12 8.22386 12 8.5V12H3V3L6.5 3C6.77614 3 7 2.77614 7 2.5C7 2.22386 6.77614 2 6.5 2H3ZM12.8536 2.14645C12.9015 2.19439 12.9377 2.24964 12.9621 2.30861C12.9861 2.36669 12.9996 2.4303 13 2.497L13 2.5V2.50049V5.5C13 5.77614 12.7761 6 12.5 6C12.2239 6 12 5.77614 12 5.5V3.70711L6.85355 8.85355C6.65829 9.04882 6.34171 9.04882 6.14645 8.85355C5.95118 8.65829 5.95118 8.34171 6.14645 8.14645L11.2929 3H9.5C9.22386 3 9 2.77614 9 2.5C9 2.22386 9.22386 2 9.5 2H12.4999H12.5C12.5678 2 12.6324 2.01349 12.6914 2.03794C12.7504 2.06234 12.8056 2.09851 12.8536 2.14645Z\" fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"></path></svg></div><span class=\"block\">") 70 + if templ_7745c5c3_Err != nil { 71 + return templ_7745c5c3_Err 72 + } 73 + var templ_7745c5c3_Var4 string 74 + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(name) 75 + if templ_7745c5c3_Err != nil { 76 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 30, Col: 29} 77 + } 78 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 79 + if templ_7745c5c3_Err != nil { 80 + return templ_7745c5c3_Err 81 + } 82 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span></a></li>") 83 + if templ_7745c5c3_Err != nil { 84 + return templ_7745c5c3_Err 85 + } 86 + return nil 87 + }) 88 + } 89 + 90 + func renderGroup(name string, nodes []*document.Node) templ.Component { 91 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 92 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 93 + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { 94 + return templ_7745c5c3_CtxErr 95 + } 96 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 97 + if !templ_7745c5c3_IsBuffer { 98 + defer func() { 99 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 100 + if templ_7745c5c3_Err == nil { 101 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 102 + } 103 + }() 104 + } 105 + ctx = templ.InitializeContext(ctx) 106 + templ_7745c5c3_Var5 := templ.GetChildren(ctx) 107 + if templ_7745c5c3_Var5 == nil { 108 + templ_7745c5c3_Var5 = templ.NopComponent 109 + } 110 + ctx = templ.ClearChildren(ctx) 111 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<li class=\"col-span-full grid grid-cols-subgrid gap-4\"><h2 class=\"text-lg font-bold col-span-full pt-4\">") 112 + if templ_7745c5c3_Err != nil { 113 + return templ_7745c5c3_Err 114 + } 115 + var templ_7745c5c3_Var6 string 116 + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(name) 117 + if templ_7745c5c3_Err != nil { 118 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 37, Col: 57} 119 + } 120 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 121 + if templ_7745c5c3_Err != nil { 122 + return templ_7745c5c3_Err 123 + } 124 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h2><ul class=\"col-span-full grid grid-cols-subgrid gap-4\">") 125 + if templ_7745c5c3_Err != nil { 126 + return templ_7745c5c3_Err 127 + } 128 + for _, node := range nodes { 129 + if node.Name.ValueString() == "group" { 130 + templ_7745c5c3_Err = renderGroup(node.Arguments[0].ValueString(), node.Children).Render(ctx, templ_7745c5c3_Buffer) 131 + if templ_7745c5c3_Err != nil { 132 + return templ_7745c5c3_Err 133 + } 134 + } else if node.Name.String() == "link" { 135 + url, _ := node.Properties.Get("url") 136 + templ_7745c5c3_Err = renderLink(node.Arguments[0].ValueString(), url.ValueString()).Render(ctx, templ_7745c5c3_Buffer) 137 + if templ_7745c5c3_Err != nil { 138 + return templ_7745c5c3_Err 139 + } 140 + } 141 + } 142 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</ul></li>") 143 + if templ_7745c5c3_Err != nil { 144 + return templ_7745c5c3_Err 145 + } 146 + return nil 147 + }) 148 + } 149 + 150 + func Authenticate() templ.Component { 151 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 152 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 153 + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { 154 + return templ_7745c5c3_CtxErr 155 + } 156 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 157 + if !templ_7745c5c3_IsBuffer { 158 + defer func() { 159 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 160 + if templ_7745c5c3_Err == nil { 161 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 162 + } 163 + }() 164 + } 165 + ctx = templ.InitializeContext(ctx) 166 + templ_7745c5c3_Var7 := templ.GetChildren(ctx) 167 + if templ_7745c5c3_Var7 == nil { 168 + templ_7745c5c3_Var7 = templ.NopComponent 169 + } 170 + ctx = templ.ClearChildren(ctx) 171 + templ_7745c5c3_Var8 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 172 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 173 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 174 + if !templ_7745c5c3_IsBuffer { 175 + defer func() { 176 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 177 + if templ_7745c5c3_Err == nil { 178 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 179 + } 180 + }() 181 + } 182 + ctx = templ.InitializeContext(ctx) 183 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"w-screen h-screen flex items-center justify-center\"><main class=\"w-full max-w-xs bg-surface rounded-lg p-4\"><p class=\"mb-3 font-serif\">Can I see some ID, please?</p><a href=\"/login\" class=\"block text-center text-sm rounded py-2 px-2 bg-foam/20 text-foam\">Log In with OIDC</a></main></div>") 184 + if templ_7745c5c3_Err != nil { 185 + return templ_7745c5c3_Err 186 + } 187 + return nil 188 + }) 189 + templ_7745c5c3_Err = Document("Log In").Render(templ.WithChildren(ctx, templ_7745c5c3_Var8), templ_7745c5c3_Buffer) 190 + if templ_7745c5c3_Err != nil { 191 + return templ_7745c5c3_Err 192 + } 193 + return nil 194 + }) 195 + } 196 + 197 + func Links(username string, doc *document.Document) templ.Component { 198 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 199 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 200 + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { 201 + return templ_7745c5c3_CtxErr 202 + } 203 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 204 + if !templ_7745c5c3_IsBuffer { 205 + defer func() { 206 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 207 + if templ_7745c5c3_Err == nil { 208 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 209 + } 210 + }() 211 + } 212 + ctx = templ.InitializeContext(ctx) 213 + templ_7745c5c3_Var9 := templ.GetChildren(ctx) 214 + if templ_7745c5c3_Var9 == nil { 215 + templ_7745c5c3_Var9 = templ.NopComponent 216 + } 217 + ctx = templ.ClearChildren(ctx) 218 + templ_7745c5c3_Var10 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 219 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 220 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 221 + if !templ_7745c5c3_IsBuffer { 222 + defer func() { 223 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 224 + if templ_7745c5c3_Err == nil { 225 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 226 + } 227 + }() 228 + } 229 + ctx = templ.InitializeContext(ctx) 230 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"flex items-center justify-between px-3 py-2 text-subtle text-xs mb-4\"><div>Howdy, <span class=\"font-mono text-gold\">") 231 + if templ_7745c5c3_Err != nil { 232 + return templ_7745c5c3_Err 233 + } 234 + var templ_7745c5c3_Var11 string 235 + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(username) 236 + if templ_7745c5c3_Err != nil { 237 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 65, Col: 59} 238 + } 239 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) 240 + if templ_7745c5c3_Err != nil { 241 + return templ_7745c5c3_Err 242 + } 243 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span></div><a href=\"/logout\" class=\"underline text-iris\">Log Out</a></div><ul class=\"grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 px-3 gap-x-4 gap-y-8 max-w-screen-xs sm:max-w-screen-sm md:max-w-screen-md lg:max-w-screen-lg mx-auto\">") 244 + if templ_7745c5c3_Err != nil { 245 + return templ_7745c5c3_Err 246 + } 247 + for _, node := range doc.Nodes { 248 + if node.Name.ValueString() == "group" { 249 + templ_7745c5c3_Err = renderGroup(node.Arguments[0].ValueString(), node.Children).Render(ctx, templ_7745c5c3_Buffer) 250 + if templ_7745c5c3_Err != nil { 251 + return templ_7745c5c3_Err 252 + } 253 + } else if node.Name.ValueString() == "link" { 254 + url, _ := node.Properties.Get("url") 255 + templ_7745c5c3_Err = renderLink(node.Arguments[0].ValueString(), url.ValueString()).Render(ctx, templ_7745c5c3_Buffer) 256 + if templ_7745c5c3_Err != nil { 257 + return templ_7745c5c3_Err 258 + } 259 + } 260 + } 261 + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</ul>") 262 + if templ_7745c5c3_Err != nil { 263 + return templ_7745c5c3_Err 264 + } 265 + return nil 266 + }) 267 + templ_7745c5c3_Err = Document("Links").Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer) 268 + if templ_7745c5c3_Err != nil { 269 + return templ_7745c5c3_Err 270 + } 271 + return nil 272 + }) 273 + } 274 + 275 + var _ = templruntime.GeneratedTemplate