mirror of
https://github.com/1f349/lavender.git
synced 2024-12-21 23:24:09 +00:00
Improve page templates and custom template loading
This commit is contained in:
parent
e06c23eec4
commit
ba56a628d0
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,3 +2,5 @@
|
|||||||
*.local
|
*.local
|
||||||
.idea/
|
.idea/
|
||||||
.data/
|
.data/
|
||||||
|
node_modules/
|
||||||
|
pages/style-minify.css
|
||||||
|
2
go.mod
2
go.mod
@ -27,7 +27,7 @@ require (
|
|||||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.15.11 // indirect
|
github.com/klauspost/compress v1.17.8 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -75,8 +75,8 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d
|
|||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
|
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||||
github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
|
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
3
pages/header.go.html
Normal file
3
pages/header.go.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<header>
|
||||||
|
<h1>{{.ServiceName}}</h1>
|
||||||
|
</header>
|
@ -2,12 +2,11 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{.ServiceName}}</title>
|
<title>{{.ServiceName}}</title>
|
||||||
<link rel="stylesheet" href="/theme/style.css">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
{{template "header.go.html" .}}
|
||||||
<h1>{{.ServiceName}}</h1>
|
|
||||||
</header>
|
|
||||||
<main>
|
<main>
|
||||||
<div>Not logged in</div>
|
<div>Not logged in</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -2,14 +2,13 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{.ServiceName}}</title>
|
<title>{{.ServiceName}}</title>
|
||||||
<link rel="stylesheet" href="/theme/style.css">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
{{template "header.go.html" .}}
|
||||||
<h1>{{.ServiceName}}</h1>
|
|
||||||
</header>
|
|
||||||
<main>
|
<main>
|
||||||
<div>Logged in as: {{.DisplayName}} ({{.Subject}})</div>
|
<div>Logged in as: {{.Auth.UserInfo.name}} ({{.Auth.Subject}})</div>
|
||||||
<div>
|
<div>
|
||||||
<form method="GET" action="/manage/apps">
|
<form method="GET" action="/manage/apps">
|
||||||
<button type="submit">Manage Applications</button>
|
<button type="submit">Manage Applications</button>
|
||||||
|
@ -2,12 +2,11 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{.ServiceName}}</title>
|
<title>{{.ServiceName}}</title>
|
||||||
<link rel="stylesheet" href="/theme/style.css">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
{{template "header.go.html" .}}
|
||||||
<h1>{{.ServiceName}}</h1>
|
|
||||||
</header>
|
|
||||||
<main>
|
<main>
|
||||||
<div>Log in as: <span>{{.LoginName}}</span></div>
|
<div>Log in as: <span>{{.LoginName}}</span></div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -2,12 +2,11 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{.ServiceName}}</title>
|
<title>{{.ServiceName}}</title>
|
||||||
<link rel="stylesheet" href="/theme/style.css">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
{{template "header.go.html" .}}
|
||||||
<h1>{{.ServiceName}}</h1>
|
|
||||||
</header>
|
|
||||||
<main>
|
<main>
|
||||||
<form method="POST" action="/login">
|
<form method="POST" action="/login">
|
||||||
<input type="hidden" name="redirect" value="{{.Redirect}}"/>
|
<input type="hidden" name="redirect" value="{{.Redirect}}"/>
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{.ServiceName}}</title>
|
<title>{{.ServiceName}}</title>
|
||||||
<link rel="stylesheet" href="/theme/style.css">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener("load", function () {
|
window.addEventListener("load", function () {
|
||||||
selectText("app-secret");
|
selectText("app-secret");
|
||||||
@ -29,9 +30,7 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
{{template "header.go.html" .}}
|
||||||
<h1>{{.ServiceName}}</h1>
|
|
||||||
</header>
|
|
||||||
<main>
|
<main>
|
||||||
<form method="GET" action="/">
|
<form method="GET" action="/">
|
||||||
<button type="submit">Home</button>
|
<button type="submit">Home</button>
|
||||||
|
@ -2,12 +2,11 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{.ServiceName}}</title>
|
<title>{{.ServiceName}}</title>
|
||||||
<link rel="stylesheet" href="/theme/style.css">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
{{template "header.go.html" .}}
|
||||||
<h1>{{.ServiceName}}</h1>
|
|
||||||
</header>
|
|
||||||
<main>
|
<main>
|
||||||
<form method="GET" action="/">
|
<form method="GET" action="/">
|
||||||
<button type="submit">Home</button>
|
<button type="submit">Home</button>
|
||||||
|
@ -2,12 +2,11 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{.ServiceName}}</title>
|
<title>{{.ServiceName}}</title>
|
||||||
<link rel="stylesheet" href="/theme/style.css">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
{{template "header.go.html" .}}
|
||||||
<h1>{{.ServiceName}}</h1>
|
|
||||||
</header>
|
|
||||||
<main>
|
<main>
|
||||||
<form method="POST" action="/authorize">
|
<form method="POST" action="/authorize">
|
||||||
<div>The application {{.AppName}} wants to access your account ({{.DisplayName}}). It requests the following permissions:</div>
|
<div>The application {{.AppName}} wants to access your account ({{.DisplayName}}). It requests the following permissions:</div>
|
||||||
|
@ -1,34 +1,36 @@
|
|||||||
package pages
|
package pages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/1f349/lavender/logger"
|
"github.com/1f349/lavender/logger"
|
||||||
|
"github.com/1f349/lavender/utils"
|
||||||
"github.com/1f349/overlapfs"
|
"github.com/1f349/overlapfs"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed *.go.html
|
//go:embed *.go.html assets/*.css
|
||||||
wwwPages embed.FS
|
wwwPages embed.FS
|
||||||
wwwTemplates *template.Template
|
wwwTemplates *template.Template
|
||||||
loadOnce sync.Once
|
loadOnce utils.Once[error]
|
||||||
|
cssAssetMap = make(map[string][]byte)
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadPages(wd string) (err error) {
|
func LoadPages(wd string) error {
|
||||||
loadOnce.Do(func() {
|
return loadOnce.Do(func() (err error) {
|
||||||
var o fs.FS = wwwPages
|
var o fs.FS = wwwPages
|
||||||
if wd != "" {
|
if wd != "" {
|
||||||
wwwDir := filepath.Join(wd, "www")
|
wwwDir := filepath.Join(wd, "www")
|
||||||
err = os.Mkdir(wwwDir, os.ModePerm)
|
err = os.Mkdir(wwwDir, os.ModePerm)
|
||||||
if err != nil && !errors.Is(err, os.ErrExist) {
|
if err != nil && !errors.Is(err, os.ErrExist) {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
wdFs := os.DirFS(wwwDir)
|
wdFs := os.DirFS(wwwDir)
|
||||||
o = overlapfs.OverlapFS{A: wwwPages, B: wdFs}
|
o = overlapfs.OverlapFS{A: wwwPages, B: wdFs}
|
||||||
@ -36,8 +38,19 @@ func LoadPages(wd string) (err error) {
|
|||||||
wwwTemplates, err = template.New("pages").Funcs(template.FuncMap{
|
wwwTemplates, err = template.New("pages").Funcs(template.FuncMap{
|
||||||
"emailHide": EmailHide,
|
"emailHide": EmailHide,
|
||||||
}).ParseFS(o, "*.go.html")
|
}).ParseFS(o, "*.go.html")
|
||||||
|
|
||||||
|
glob, err := fs.Glob(o, "assets/*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, i := range glob {
|
||||||
|
cssAssetMap[i], err = fs.ReadFile(o, i)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderPageTemplate(wr io.Writer, name string, data any) {
|
func RenderPageTemplate(wr io.Writer, name string, data any) {
|
||||||
@ -47,6 +60,14 @@ func RenderPageTemplate(wr io.Writer, name string, data any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RenderCss(name string) io.ReadSeeker {
|
||||||
|
b, ok := cssAssetMap[name]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return bytes.NewReader(b)
|
||||||
|
}
|
||||||
|
|
||||||
func EmailHide(a string) string {
|
func EmailHide(a string) string {
|
||||||
b := []byte(a)
|
b := []byte(a)
|
||||||
for i := range b {
|
for i := range b {
|
||||||
|
@ -2,8 +2,6 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/1f349/lavender/database"
|
"github.com/1f349/lavender/database"
|
||||||
"github.com/1f349/mjwt"
|
|
||||||
"github.com/1f349/mjwt/auth"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -13,18 +11,18 @@ import (
|
|||||||
type UserHandler func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth)
|
type UserHandler func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth)
|
||||||
|
|
||||||
type UserAuth struct {
|
type UserAuth struct {
|
||||||
ID string
|
Subject string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
UserInfo UserInfoFields
|
UserInfo UserInfoFields
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u UserAuth) IsGuest() bool { return u.ID == "" }
|
func (u UserAuth) IsGuest() bool { return u.Subject == "" }
|
||||||
|
|
||||||
func (h *HttpServer) RequireAdminAuthentication(next UserHandler) httprouter.Handle {
|
func (h *HttpServer) RequireAdminAuthentication(next UserHandler) httprouter.Handle {
|
||||||
return h.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
|
return h.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
|
||||||
var roles string
|
var roles string
|
||||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||||
roles, err = tx.GetUserRoles(auth.ID)
|
roles, err = tx.GetUserRoles(auth.Subject)
|
||||||
return
|
return
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
@ -50,29 +48,23 @@ func (h *HttpServer) RequireAuthentication(next UserHandler) httprouter.Handle {
|
|||||||
|
|
||||||
func (h *HttpServer) OptionalAuthentication(next UserHandler) httprouter.Handle {
|
func (h *HttpServer) OptionalAuthentication(next UserHandler) httprouter.Handle {
|
||||||
return func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
return func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||||
auth, err := h.internalAuthenticationHandler(req)
|
authUser, err := h.internalAuthenticationHandler(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if auth.IsGuest() {
|
next(rw, req, params, authUser)
|
||||||
// if this fails internally it just sees the user as logged out
|
|
||||||
h.readLoginDataCookie(req, &auth)
|
|
||||||
}
|
|
||||||
next(rw, req, params, auth)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) internalAuthenticationHandler(req *http.Request) (UserAuth, error) {
|
func (h *HttpServer) internalAuthenticationHandler(req *http.Request) (UserAuth, error) {
|
||||||
if loginCookie, err := req.Cookie("lavender-login-data"); err == nil {
|
var u UserAuth
|
||||||
_, b, err := mjwt.ExtractClaims[auth.AccessTokenClaims](h.signingKey, loginCookie.Value)
|
err := h.readLoginDataCookie(req, &u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UserAuth{}, err
|
// not logged in
|
||||||
}
|
return UserAuth{}, nil
|
||||||
return UserAuth{ID: b.Subject}, nil
|
|
||||||
}
|
}
|
||||||
// not logged in
|
return u, nil
|
||||||
return UserAuth{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrepareRedirectUrl(targetPath string, origin *url.URL) *url.URL {
|
func PrepareRedirectUrl(targetPath string, origin *url.URL) *url.URL {
|
||||||
|
@ -30,7 +30,7 @@ func (h *HttpServer) Home(rw http.ResponseWriter, _ *http.Request, _ httprouter.
|
|||||||
|
|
||||||
var isAdmin bool
|
var isAdmin bool
|
||||||
h.DbTx(rw, func(tx *database.Tx) (err error) {
|
h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||||
roles, err := tx.GetUserRoles(auth.ID)
|
roles, err := tx.GetUserRoles(auth.Subject)
|
||||||
isAdmin = HasRole(roles, "lavender:admin")
|
isAdmin = HasRole(roles, "lavender:admin")
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
@ -38,8 +38,6 @@ func (h *HttpServer) Home(rw http.ResponseWriter, _ *http.Request, _ httprouter.
|
|||||||
pages.RenderPageTemplate(rw, "index", map[string]any{
|
pages.RenderPageTemplate(rw, "index", map[string]any{
|
||||||
"ServiceName": h.conf.ServiceName,
|
"ServiceName": h.conf.ServiceName,
|
||||||
"Auth": auth,
|
"Auth": auth,
|
||||||
"Subject": auth.ID,
|
|
||||||
"DisplayName": auth.DisplayName,
|
|
||||||
"Nonce": lNonce,
|
"Nonce": lNonce,
|
||||||
"IsAdmin": isAdmin,
|
"IsAdmin": isAdmin,
|
||||||
})
|
})
|
||||||
|
@ -2,17 +2,14 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha256"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/1f349/lavender/database"
|
"github.com/1f349/lavender/database"
|
||||||
"github.com/1f349/lavender/issuer"
|
"github.com/1f349/lavender/issuer"
|
||||||
"github.com/1f349/lavender/pages"
|
"github.com/1f349/lavender/pages"
|
||||||
|
"github.com/1f349/mjwt"
|
||||||
"github.com/1f349/mjwt/auth"
|
"github.com/1f349/mjwt/auth"
|
||||||
"github.com/1f349/mjwt/claims"
|
"github.com/1f349/mjwt/claims"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v4"
|
||||||
@ -112,7 +109,7 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _
|
|||||||
}
|
}
|
||||||
|
|
||||||
sessionData, err := h.fetchUserInfo(flowState.sso, token)
|
sessionData, err := h.fetchUserInfo(flowState.sso, token)
|
||||||
if sessionData.ID == "" {
|
if err != nil || sessionData.Subject == "" {
|
||||||
http.Error(rw, "Failed to fetch user info", http.StatusInternalServerError)
|
http.Error(rw, "Failed to fetch user info", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -122,15 +119,15 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = tx.GetUser(sessionData.ID)
|
_, err = tx.GetUser(sessionData.Subject)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
uEmail := sessionData.UserInfo.GetStringOrDefault("email", "unknown@localhost")
|
uEmail := sessionData.UserInfo.GetStringOrDefault("email", "unknown@localhost")
|
||||||
uEmailVerified, _ := sessionData.UserInfo.GetBoolean("email_verified")
|
uEmailVerified, _ := sessionData.UserInfo.GetBoolean("email_verified")
|
||||||
return tx.InsertUser(sessionData.ID, uEmail, uEmailVerified, "", string(jBytes), true)
|
return tx.InsertUser(sessionData.Subject, uEmail, uEmailVerified, "", string(jBytes), true)
|
||||||
}
|
}
|
||||||
uEmail := sessionData.UserInfo.GetStringOrDefault("email", "unknown@localhost")
|
uEmail := sessionData.UserInfo.GetStringOrDefault("email", "unknown@localhost")
|
||||||
uEmailVerified, _ := sessionData.UserInfo.GetBoolean("email_verified")
|
uEmailVerified, _ := sessionData.UserInfo.GetBoolean("email_verified")
|
||||||
return tx.UpdateUserInfo(sessionData.ID, uEmail, uEmailVerified, string(jBytes))
|
return tx.UpdateUserInfo(sessionData.Subject, uEmail, uEmailVerified, string(jBytes))
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -139,7 +136,7 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _
|
|||||||
auth = sessionData
|
auth = sessionData
|
||||||
|
|
||||||
if h.DbTx(rw, func(tx *database.Tx) error {
|
if h.DbTx(rw, func(tx *database.Tx) error {
|
||||||
return tx.UpdateUserToken(auth.ID, token.AccessToken, token.RefreshToken, token.Expiry)
|
return tx.UpdateUserToken(auth.Subject, token.AccessToken, token.RefreshToken, token.Expiry)
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -156,9 +153,21 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _
|
|||||||
|
|
||||||
const oneYear = 365 * 24 * time.Hour
|
const oneYear = 365 * 24 * time.Hour
|
||||||
|
|
||||||
|
type lavenderLoginData struct {
|
||||||
|
UserInfo UserInfoFields `json:"user_info"`
|
||||||
|
auth.AccessTokenClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l lavenderLoginData) Valid() error { return nil }
|
||||||
|
|
||||||
|
func (l lavenderLoginData) Type() string { return "lavender-login-data" }
|
||||||
|
|
||||||
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, authData UserAuth) bool {
|
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, authData UserAuth) bool {
|
||||||
ps := claims.NewPermStorage()
|
ps := claims.NewPermStorage()
|
||||||
gen, err := h.signingKey.GenerateJwt(authData.ID, uuid.NewString(), jwt.ClaimStrings{h.conf.BaseUrl}, oneYear, auth.AccessTokenClaims{Perms: ps})
|
gen, err := h.signingKey.GenerateJwt(authData.Subject, uuid.NewString(), jwt.ClaimStrings{h.conf.BaseUrl}, oneYear, lavenderLoginData{
|
||||||
|
UserInfo: authData.UserInfo,
|
||||||
|
AccessTokenClaims: auth.AccessTokenClaims{Perms: ps},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(rw, "Failed to generate cookie token", http.StatusInternalServerError)
|
http.Error(rw, "Failed to generate cookie token", http.StatusInternalServerError)
|
||||||
return true
|
return true
|
||||||
@ -174,34 +183,20 @@ func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, authData UserAut
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) readLoginDataCookie(req *http.Request, u *UserAuth) {
|
func (h *HttpServer) readLoginDataCookie(req *http.Request, u *UserAuth) error {
|
||||||
loginCookie, err := req.Cookie("lavender-login-data")
|
loginCookie, err := req.Cookie("lavender-login-data")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
hexData, err := hex.DecodeString(loginCookie.Value)
|
_, b, err := mjwt.ExtractClaims[lavenderLoginData](h.signingKey, loginCookie.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
decData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, h.signingKey.PrivateKey(), hexData, []byte("lavender-login-data"))
|
*u = UserAuth{
|
||||||
if err != nil {
|
Subject: b.Subject,
|
||||||
return
|
UserInfo: b.Claims.UserInfo,
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
userId := string(decData)
|
|
||||||
var token oauth2.Token
|
|
||||||
if h.DbTxRaw(func(tx *database.Tx) error {
|
|
||||||
return tx.GetUserToken(userId, &token.AccessToken, &token.RefreshToken, &token.Expiry)
|
|
||||||
}) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sso := h.manager.FindServiceFromLogin(userId)
|
|
||||||
if sso == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
*u, _ = h.fetchUserInfo(sso, &token)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) fetchUserInfo(sso *issuer.WellKnownOIDC, token *oauth2.Token) (UserAuth, error) {
|
func (h *HttpServer) fetchUserInfo(sso *issuer.WellKnownOIDC, token *oauth2.Token) (UserAuth, error) {
|
||||||
@ -221,10 +216,8 @@ func (h *HttpServer) fetchUserInfo(sso *issuer.WellKnownOIDC, token *oauth2.Toke
|
|||||||
}
|
}
|
||||||
subject += "@" + sso.Config.Namespace
|
subject += "@" + sso.Config.Namespace
|
||||||
|
|
||||||
displayName := userInfoJson.GetStringOrDefault("name", "Unknown Name")
|
|
||||||
return UserAuth{
|
return UserAuth{
|
||||||
ID: subject,
|
Subject: subject,
|
||||||
DisplayName: displayName,
|
UserInfo: userInfoJson,
|
||||||
UserInfo: userInfoJson,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -26,11 +26,11 @@ func (h *HttpServer) ManageAppsGet(rw http.ResponseWriter, req *http.Request, _
|
|||||||
var roles string
|
var roles string
|
||||||
var appList []database.ClientInfoDbOutput
|
var appList []database.ClientInfoDbOutput
|
||||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||||
roles, err = tx.GetUserRoles(auth.ID)
|
roles, err = tx.GetUserRoles(auth.Subject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
appList, err = tx.GetAppList(auth.ID, HasRole(roles, "lavender:admin"), offset)
|
appList, err = tx.GetAppList(auth.Subject, HasRole(roles, "lavender:admin"), offset)
|
||||||
return
|
return
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
@ -80,7 +80,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
|
|||||||
if sso || hasPerms {
|
if sso || hasPerms {
|
||||||
var roles string
|
var roles string
|
||||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||||
roles, err = tx.GetUserRoles(auth.ID)
|
roles, err = tx.GetUserRoles(auth.Subject)
|
||||||
return
|
return
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
@ -98,7 +98,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
|
|||||||
switch action {
|
switch action {
|
||||||
case "create":
|
case "create":
|
||||||
if h.DbTx(rw, func(tx *database.Tx) error {
|
if h.DbTx(rw, func(tx *database.Tx) error {
|
||||||
return tx.InsertClientApp(name, domain, auth.ID, perms, public, sso, active)
|
return tx.InsertClientApp(name, domain, auth.Subject, perms, public, sso, active)
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -108,7 +108,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return tx.UpdateClientApp(sub, auth.ID, name, domain, perms, hasPerms, public, sso, active)
|
return tx.UpdateClientApp(sub, auth.Subject, name, domain, perms, hasPerms, public, sso, active)
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -124,7 +124,7 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
secret, err = tx.ResetClientAppSecret(sub, auth.ID)
|
secret, err = tx.ResetClientAppSecret(sub, auth.Subject)
|
||||||
return err
|
return err
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
|
@ -24,7 +24,7 @@ func (h *HttpServer) ManageUsersGet(rw http.ResponseWriter, req *http.Request, _
|
|||||||
var roles string
|
var roles string
|
||||||
var userList []database.User
|
var userList []database.User
|
||||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||||
roles, err = tx.GetUserRoles(auth.ID)
|
roles, err = tx.GetUserRoles(auth.Subject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -43,7 +43,7 @@ func (h *HttpServer) ManageUsersGet(rw http.ResponseWriter, req *http.Request, _
|
|||||||
"Users": userList,
|
"Users": userList,
|
||||||
"Offset": offset,
|
"Offset": offset,
|
||||||
"EmailShow": req.URL.Query().Has("show-email"),
|
"EmailShow": req.URL.Query().Has("show-email"),
|
||||||
"CurrentAdmin": auth.ID,
|
"CurrentAdmin": auth.Subject,
|
||||||
}
|
}
|
||||||
if q.Has("edit") {
|
if q.Has("edit") {
|
||||||
for _, i := range userList {
|
for _, i := range userList {
|
||||||
@ -71,7 +71,7 @@ func (h *HttpServer) ManageUsersPost(rw http.ResponseWriter, req *http.Request,
|
|||||||
|
|
||||||
var roles string
|
var roles string
|
||||||
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
if h.DbTx(rw, func(tx *database.Tx) (err error) {
|
||||||
roles, err = tx.GetUserRoles(auth.ID)
|
roles, err = tx.GetUserRoles(auth.Subject)
|
||||||
return
|
return
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
|
@ -147,5 +147,5 @@ func (h *HttpServer) oauthUserAuthorization(rw http.ResponseWriter, req *http.Re
|
|||||||
http.Redirect(rw, req, redirectUrl.String(), http.StatusFound)
|
http.Redirect(rw, req, redirectUrl.String(), http.StatusFound)
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
return auth.ID, nil
|
return auth.Subject, nil
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/1f349/cache"
|
"github.com/1f349/cache"
|
||||||
@ -10,8 +9,8 @@ import (
|
|||||||
"github.com/1f349/lavender/issuer"
|
"github.com/1f349/lavender/issuer"
|
||||||
"github.com/1f349/lavender/logger"
|
"github.com/1f349/lavender/logger"
|
||||||
"github.com/1f349/lavender/openid"
|
"github.com/1f349/lavender/openid"
|
||||||
|
"github.com/1f349/lavender/pages"
|
||||||
scope2 "github.com/1f349/lavender/scope"
|
scope2 "github.com/1f349/lavender/scope"
|
||||||
"github.com/1f349/lavender/theme"
|
|
||||||
"github.com/1f349/mjwt"
|
"github.com/1f349/mjwt"
|
||||||
"github.com/go-oauth2/oauth2/v4/errors"
|
"github.com/go-oauth2/oauth2/v4/errors"
|
||||||
"github.com/go-oauth2/oauth2/v4/manage"
|
"github.com/go-oauth2/oauth2/v4/manage"
|
||||||
@ -20,6 +19,7 @@ import (
|
|||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -44,6 +44,7 @@ type flowStateData struct {
|
|||||||
|
|
||||||
func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Server {
|
func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Server {
|
||||||
r := httprouter.New()
|
r := httprouter.New()
|
||||||
|
contentCache := time.Now()
|
||||||
|
|
||||||
// remove last slash from baseUrl
|
// remove last slash from baseUrl
|
||||||
{
|
{
|
||||||
@ -139,8 +140,14 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// theme styles
|
// theme styles
|
||||||
r.GET("/theme/style.css", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
r.GET("/assets/*filepath", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||||
http.ServeContent(rw, req, "style.css", time.Now(), bytes.NewReader(theme.DefaultThemeCss))
|
name := params.ByName("filepath")
|
||||||
|
if strings.Contains(name, "..") {
|
||||||
|
http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := pages.RenderCss(path.Join("assets", name))
|
||||||
|
http.ServeContent(rw, req, path.Base(name), contentCache, out)
|
||||||
})
|
})
|
||||||
|
|
||||||
// management pages
|
// management pages
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
package theme
|
|
||||||
|
|
||||||
import _ "embed"
|
|
||||||
|
|
||||||
//go:embed style.css
|
|
||||||
var DefaultThemeCss []byte
|
|
15
utils/once.go
Normal file
15
utils/once.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type Once[T any] struct {
|
||||||
|
once sync.Once
|
||||||
|
value T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Once[T]) Do(f func() T) T {
|
||||||
|
o.once.Do(func() {
|
||||||
|
o.value = f()
|
||||||
|
})
|
||||||
|
return o.value
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user