mirror of
https://github.com/1f349/lavender.git
synced 2025-01-21 06:06:30 +00:00
Add layouts from tulip
This commit is contained in:
parent
611cb5c6d5
commit
c96d2dc446
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@ -0,0 +1,17 @@
|
||||
# EditorConfig is awesome: https://editorconfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
24
auth/auth.go
24
auth/auth.go
@ -5,16 +5,18 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/1f349/lavender/database"
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Factor byte
|
||||
|
||||
const (
|
||||
// FactorAuthorized defines the "authorized" state of a session
|
||||
FactorAuthorized Factor = iota
|
||||
FactorFirst
|
||||
FactorSecond
|
||||
// FactorUnauthorized defines the "unauthorized" state of a session
|
||||
FactorUnauthorized Factor = iota
|
||||
FactorBasic
|
||||
FactorExtended
|
||||
FactorSudo
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
@ -25,18 +27,20 @@ type Provider interface {
|
||||
// Name defines a string value for the provider, useful for template switching
|
||||
Name() string
|
||||
|
||||
// RenderData stores values to send to the templating function
|
||||
RenderData(ctx context.Context, req *http.Request, user *database.User, data map[string]any) error
|
||||
// RenderTemplate returns HTML to embed in the page template
|
||||
RenderTemplate(ctx context.Context, req *http.Request, user *database.User) (template.HTML, error)
|
||||
|
||||
// AttemptLogin processes the login request
|
||||
AttemptLogin(ctx context.Context, req *http.Request, user *database.User) error
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrRequiresSecondFactor notifies the ServeHTTP function to ask for another factor
|
||||
ErrRequiresSecondFactor = errors.New("requires second factor")
|
||||
// ErrRequiresPreviousFactor is a generic error for providers which require a previous factor
|
||||
ErrRequiresPreviousFactor = errors.New("requires previous factor")
|
||||
// ErrRequiresBasicFactor notifies the ServeHTTP function to ask for another factor
|
||||
ErrRequiresBasicFactor = errors.New("requires basic factor")
|
||||
// ErrRequiresExtendedFactor is a generic error for providers which require a previous factor
|
||||
ErrRequiresExtendedFactor = errors.New("requires extended factor")
|
||||
|
||||
ErrRequiresSudoFactor = errors.New("requires sudo factor")
|
||||
// ErrUserDoesNotSupportFactor is a generic error for providers with are unable to support the user
|
||||
ErrUserDoesNotSupportFactor = errors.New("user does not support factor")
|
||||
)
|
||||
|
@ -19,7 +19,7 @@ type BasicLogin struct {
|
||||
DB basicLoginDB
|
||||
}
|
||||
|
||||
func (b *BasicLogin) Factor() Factor { return FactorFirst }
|
||||
func (b *BasicLogin) Factor() Factor { return FactorBasic }
|
||||
|
||||
func (b *BasicLogin) Name() string { return "basic" }
|
||||
|
||||
|
@ -32,7 +32,7 @@ func (o OAuthLogin) Init() {
|
||||
o.flow = cache.New[string, flowStateData]()
|
||||
}
|
||||
|
||||
func (o OAuthLogin) Factor() Factor { return FactorFirst }
|
||||
func (o OAuthLogin) Factor() Factor { return FactorBasic }
|
||||
|
||||
func (o OAuthLogin) Name() string { return "oauth" }
|
||||
|
||||
|
@ -23,7 +23,7 @@ type OtpLogin struct {
|
||||
DB otpLoginDB
|
||||
}
|
||||
|
||||
func (o *OtpLogin) Factor() Factor { return FactorSecond }
|
||||
func (o *OtpLogin) Factor() Factor { return FactorExtended }
|
||||
|
||||
func (o *OtpLogin) Name() string { return "basic" }
|
||||
|
||||
|
@ -16,7 +16,7 @@ type PasskeyLogin struct {
|
||||
DB passkeyLoginDB
|
||||
}
|
||||
|
||||
func (p *PasskeyLogin) Factor() Factor { return FactorFirst }
|
||||
func (p *PasskeyLogin) Factor() Factor { return FactorBasic }
|
||||
|
||||
func (p *PasskeyLogin) Name() string { return "passkey" }
|
||||
|
||||
|
@ -49,7 +49,7 @@ func (h *httpServer) testAuthSources(req *http.Request, user *database.User, fac
|
||||
if i.Factor()&factor == 0 {
|
||||
continue
|
||||
}
|
||||
err := i.RenderData(req.Context(), req, user, data)
|
||||
err := i.RenderTemplate(req.Context(), req, user, data)
|
||||
authSource[i.Name()] = err == nil
|
||||
clear(data)
|
||||
}
|
||||
@ -76,14 +76,14 @@ func (h *httpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httpr
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%#v\n", h.testAuthSources(req, userPtr, auth2.FactorFirst))
|
||||
fmt.Printf("%#v\n", h.testAuthSources(req, userPtr, auth2.FactorBasic))
|
||||
|
||||
web.RenderPageTemplate(rw, "login-memory", map[string]any{
|
||||
"ServiceName": h.conf.ServiceName,
|
||||
"LoginName": cookie.Value,
|
||||
"Redirect": req.URL.Query().Get("redirect"),
|
||||
"Source": "start",
|
||||
"Auth": h.testAuthSources(req, userPtr, auth2.FactorFirst),
|
||||
"Auth": h.testAuthSources(req, userPtr, auth2.FactorBasic),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -94,7 +94,7 @@ func (h *httpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httpr
|
||||
"LoginName": "",
|
||||
"Redirect": req.URL.Query().Get("redirect"),
|
||||
"Source": "start",
|
||||
"Auth": h.testAuthSources(req, nil, auth2.FactorFirst),
|
||||
"Auth": h.testAuthSources(req, nil, auth2.FactorBasic),
|
||||
})
|
||||
}
|
||||
|
||||
|
6
web/.editorconfig
Normal file
6
web/.editorconfig
Normal file
@ -0,0 +1,6 @@
|
||||
# EditorConfig is awesome: https://editorconfig.org
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.js]
|
||||
charset = utf-8
|
3
web/src/components/Header.astro
Normal file
3
web/src/components/Header.astro
Normal file
@ -0,0 +1,3 @@
|
||||
<header>
|
||||
<h1>[[.ServiceName]]</h1>
|
||||
</header>
|
@ -17,7 +17,9 @@ const { title } = Astro.props;
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
<style is:global>
|
||||
|
27
web/src/pages/edit-otp.go.html
Normal file
27
web/src/pages/edit-otp.go.html
Normal file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<link rel="stylesheet" href="/theme/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>{{.ServiceName}}</h1>
|
||||
</header>
|
||||
<main>
|
||||
<form method="POST" action="/edit/otp">
|
||||
<input type="hidden" name="secret" value="{{.OtpSecret}}"/>
|
||||
<input type="hidden" name="digits" value="{{.OtpDigits}}"/>
|
||||
<p>
|
||||
<img src="{{.OtpQr}}" style="width:{{.QrWidth}}px" alt="OTP QR code not loading"/>
|
||||
</p>
|
||||
<p style="display:none">Raw OTP string: {{.OtpUrl}}</p>
|
||||
<div>
|
||||
<label for="field_code">OTP Code:</label>
|
||||
<input type="text" name="code" id="field_code" required autofocus pattern="[0-9]{6,8}" title="6/7/8 digit one time passcode"/>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
29
web/src/pages/edit-password.go.html
Normal file
29
web/src/pages/edit-password.go.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<link rel="stylesheet" href="/theme/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>{{.ServiceName}}</h1>
|
||||
</header>
|
||||
<main>
|
||||
<form method="POST" action="/edit/password">
|
||||
<div>
|
||||
<label for="field_password">Current Password:</label>
|
||||
<input type="password" name="password" id="field_password" autocomplete="password" autofocus required/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_password">New Password:</label>
|
||||
<input type="password" name="password" id="field_password" autocomplete="new_password" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_password">Retype New Password:</label>
|
||||
<input type="password" name="password" id="field_password" autocomplete="confirm_password" required/>
|
||||
</div>
|
||||
<button type="submit">Change Password</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
72
web/src/pages/edit.go.html
Normal file
72
web/src/pages/edit.go.html
Normal file
@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<link rel="stylesheet" href="/theme/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>{{.ServiceName}}</h1>
|
||||
</header>
|
||||
<main>
|
||||
<div>Logged in as: {{.User.Name}} ({{.User.Subject}})</div>
|
||||
<div>
|
||||
<form method="POST" action="/edit">
|
||||
<input type="hidden" name="nonce" value="{{.Nonce}}">
|
||||
<div>
|
||||
<label for="field_name">Name:</label>
|
||||
<input type="text" name="name" id="field_name" value="{{.User.Name}}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_picture">Picture:</label>
|
||||
<input type="text" name="picture" id="field_picture" value="{{.User.Picture}}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_website">Website:</label>
|
||||
<input type="text" name="website" id="field_website" value="{{.User.Website}}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_pronouns">Pronouns:</label>
|
||||
<select name="pronouns" id="field_pronouns">
|
||||
<option value="they/them" {{if eq "they/them" .FieldPronoun}}selected{{end}}>They/Them</option>
|
||||
<option value="he/him" {{if eq "he/him" .FieldPronoun}}selected{{end}}>He/Him</option>
|
||||
<option value="she/her" {{if eq "she/her" .FieldPronoun}}selected{{end}}>She/Her</option>
|
||||
<option value="it/its" {{if eq "it/its" .FieldPronoun}}selected{{end}}>It/Its</option>
|
||||
<option value="one/one's" {{if eq "one/one's" .FieldPronoun}}selected{{end}}>One/One's</option>
|
||||
</select>
|
||||
<label>Reset? <input type="checkbox" name="reset_pronouns"></label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_birthdate">Birthdate:</label>
|
||||
<input type="date" name="birthdate" id="field_birthdate" value="{{.User.Birthdate}}">
|
||||
<label>Reset? <input type="checkbox" name="reset_birthdate"></label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_zoneinfo">Time Zone:</label>
|
||||
<input type="text" name="zoneinfo" id="field_zoneinfo" value="{{.User.Zoneinfo}}" list="list_zoneinfo">
|
||||
<datalist id="list_zoneinfo">
|
||||
{{range .ListZoneInfo}}
|
||||
<option value="{{.}}"></option>
|
||||
{{end}}
|
||||
</datalist>
|
||||
<label>Reset? <input type="checkbox" name="reset_zoneinfo"></label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_locale">Language:</label>
|
||||
<input type="text" name="locale" id="field_locale" value="{{.User.Locale}}" list="list_locale">
|
||||
<datalist id="list_locale">
|
||||
{{range .ListLocale}}
|
||||
<option value="{{.Value}}">{{.Label}}</option>
|
||||
{{end}}
|
||||
</datalist>
|
||||
<label>Reset? <input type="checkbox" name="reset_locale"></label>
|
||||
</div>
|
||||
<button type="submit">Edit</button>
|
||||
</form>
|
||||
<form method="GET" action="/">
|
||||
<button type="submit">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
19
web/src/pages/index-guest.go.html
Normal file
19
web/src/pages/index-guest.go.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
</head>
|
||||
<body>
|
||||
{{template "header.go.html" .}}
|
||||
<main>
|
||||
<div>Not logged in</div>
|
||||
<div>
|
||||
<form method="GET" action="/login">
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
49
web/src/pages/index.go.html
Normal file
49
web/src/pages/index.go.html
Normal file
@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
</head>
|
||||
<body>
|
||||
{{template "header.go.html" .}}
|
||||
<main>
|
||||
<div>Logged in as: {{.Auth.UserInfo.name}} ({{.Auth.Subject}})</div>
|
||||
<div>
|
||||
<form method="GET" action="/manage/apps">
|
||||
<button type="submit">Manage Applications</button>
|
||||
</form>
|
||||
</div>
|
||||
{{if .IsAdmin}}
|
||||
<div>
|
||||
<form method="GET" action="/manage/users">
|
||||
<button type="submit">Manage Users</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .OtpEnabled}}
|
||||
<div>
|
||||
<form method="POST" action="/edit/otp">
|
||||
<input type="hidden" name="remove" value="1"/>
|
||||
<button type="submit">Remove OTP</button>
|
||||
</form>
|
||||
</div>
|
||||
{{else}}
|
||||
<div>
|
||||
<form method="POST" action="/edit/otp">
|
||||
<label><input type="radio" name="digits" value="6"/> 6 digits</label>
|
||||
<label><input type="radio" name="digits" value="7"/> 7 digits</label>
|
||||
<label><input type="radio" name="digits" value="8"/> 8 digits</label>
|
||||
<button type="submit">Change OTP</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
<div>
|
||||
<form method="POST" action="/logout">
|
||||
<input type="hidden" name="nonce" value="{{.Nonce}}">
|
||||
<button type="submit">Log Out</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
26
web/src/pages/login-memory.go.html
Normal file
26
web/src/pages/login-memory.go.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
</head>
|
||||
<body>
|
||||
{{template "header.go.html" .}}
|
||||
<main>
|
||||
<div>Log in as: <span>{{.LoginName}}</span></div>
|
||||
<div>
|
||||
<form method="POST" action="/login">
|
||||
<button type="submit" name="not-you" value="1">Not You?</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form method="POST" action="/login">
|
||||
<input type="hidden" name="redirect" value="{{.Redirect}}"/>
|
||||
<input type="hidden" name="loginname" value="{{.LoginName}}"/>
|
||||
<button type="submit">Continue</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
61
web/src/pages/login.go.html
Normal file
61
web/src/pages/login.go.html
Normal file
@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
</head>
|
||||
<body>
|
||||
{{template "header.go.html" .}}
|
||||
<main>
|
||||
{{if eq .Mismatch "1"}}
|
||||
<p>Invalid username or password</p>
|
||||
{{else if eq .Mismatch "2"}}
|
||||
<p>Check your inbox for a verification email</p>
|
||||
{{end}}
|
||||
{{if eq .Source "start"}}
|
||||
<form method="POST" action="/login">
|
||||
<input type="hidden" name="redirect" value="{{.Redirect}}"/>
|
||||
<div>
|
||||
<label for="field_loginname">Login Name:</label>
|
||||
<input type="text" name="loginname" id="field_loginname" required/>
|
||||
</div>
|
||||
<button type="submit">Continue</button>
|
||||
</form>
|
||||
<!--
|
||||
<div style="display: none;">
|
||||
<button id="start-passkey-auth">Sign in with a passkey</button>
|
||||
</div>
|
||||
-->
|
||||
<form method="POST" action="/reset-password">
|
||||
<p>Enter your email address below to receive an email with instructions on how to reset your password.</p>
|
||||
<p>Please note this only works if your email address is already verified.</p>
|
||||
<div>
|
||||
<label for="field_email">Email:</label>
|
||||
<input type="email" name="email" id="field_email" required/>
|
||||
</div>
|
||||
<button type="submit">Send Reset Password Email</button>
|
||||
</form>
|
||||
{{else if eq .Source "password"}}
|
||||
<form method="POST" action="/login">
|
||||
<input type="hidden" name="redirect" value="{{.Redirect}}"/>
|
||||
<input type="hidden" name="loginname" value="{{.LoginName}}"/>
|
||||
<div>
|
||||
<label for="field_password">Password:</label>
|
||||
<input type="password" name="password" id="field_password" autofocus required/>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{{else if eq .Source "otp"}}
|
||||
<form method="POST" action="/login/otp" autocomplete="off">
|
||||
<input type="hidden" name="redirect" value="{{.Redirect}}"/>
|
||||
<div>
|
||||
<label for="field_code">OTP Code:</label>
|
||||
<input type="text" name="code" id="field_code" required pattern="[0-9]{6,8}" title="6/7/8 digit one time passcode" autocomplete="off" autofocus aria-autocomplete="none" role="presentation"/>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
72
web/src/pages/manage-apps-create.go.html
Normal file
72
web/src/pages/manage-apps-create.go.html
Normal file
@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
<script>
|
||||
window.addEventListener("load", function () {
|
||||
selectText("app-secret");
|
||||
});
|
||||
|
||||
// Thanks again: https://stackoverflow.com/a/987376
|
||||
function selectText(nodeId) {
|
||||
const node = document.getElementById(nodeId);
|
||||
|
||||
if (document.body.createTextRange) {
|
||||
const range = document.body.createTextRange();
|
||||
range.moveToElementText(node);
|
||||
range.select();
|
||||
} else if (window.getSelection) {
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
} else {
|
||||
console.warn("Could not select text in node: Unsupported browser.");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{{template "header.go.html" .}}
|
||||
<main>
|
||||
<form method="GET" action="/">
|
||||
<button type="submit">Home</button>
|
||||
</form>
|
||||
|
||||
<h2>Create Client Application</h2>
|
||||
<form method="POST" action="/manage/apps">
|
||||
<input type="hidden" name="action" value="create"/>
|
||||
<input type="hidden" name="offset" value="{{.Offset}}"/>
|
||||
<div>
|
||||
<label for="field_name">Name:</label>
|
||||
<input type="text" name="name" id="field_name" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_domain">Domain:</label>
|
||||
<input type="text" name="domain" id="field_domain" required/>
|
||||
</div>
|
||||
{{if .IsAdmin}}
|
||||
<div>
|
||||
<label for="field_perms">Perms:</label>
|
||||
<input type="text" name="perms" id="field_perms"/>
|
||||
</div>
|
||||
{{end}}
|
||||
<div>
|
||||
<label for="field_public">Public: <input type="checkbox" name="public" id="field_public"/></label>
|
||||
</div>
|
||||
{{if .IsAdmin}}
|
||||
<div>
|
||||
<label for="field_sso">SSO: <input type="checkbox" name="sso" id="field_sso"/></label>
|
||||
</div>
|
||||
{{end}}
|
||||
<div>
|
||||
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active" checked/></label>
|
||||
</div>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
80
web/src/pages/manage-apps-edit.go.html
Normal file
80
web/src/pages/manage-apps-edit.go.html
Normal file
@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
<script>
|
||||
window.addEventListener("load", function () {
|
||||
selectText("app-secret");
|
||||
});
|
||||
|
||||
// Thanks again: https://stackoverflow.com/a/987376
|
||||
function selectText(nodeId) {
|
||||
const node = document.getElementById(nodeId);
|
||||
|
||||
if (document.body.createTextRange) {
|
||||
const range = document.body.createTextRange();
|
||||
range.moveToElementText(node);
|
||||
range.select();
|
||||
} else if (window.getSelection) {
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
} else {
|
||||
console.warn("Could not select text in node: Unsupported browser.");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{{template "header.go.html" .}}
|
||||
<main>
|
||||
<form method="GET" action="/">
|
||||
<button type="submit">Home</button>
|
||||
</form>
|
||||
|
||||
<h2>Edit Client Application</h2>
|
||||
<form method="POST" action="/manage/apps">
|
||||
<input type="hidden" name="action" value="edit"/>
|
||||
<input type="hidden" name="offset" value="{{.Offset}}"/>
|
||||
<input type="hidden" name="subject" value="{{.EditApp.Subject}}"/>
|
||||
<div>
|
||||
<label>ID: {{.EditApp.Subject}}</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_name">Name:</label>
|
||||
<input type="text" name="name" id="field_name" value="{{.EditApp.Name}}" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_domain">Domain:</label>
|
||||
<input type="text" name="domain" id="field_domain" value="{{.EditApp.Domain}}" required/>
|
||||
</div>
|
||||
{{if .IsAdmin}}
|
||||
<div>
|
||||
<label for="field_perms">Perms:</label>
|
||||
<input type="text" name="perms" id="field_perms" value="{{.EditApp.Perms}}" size="100"/>
|
||||
</div>
|
||||
{{end}}
|
||||
<div>
|
||||
<label for="field_public">Public: <input type="checkbox" name="public" id="field_public" {{if .EditApp.Public}}checked{{end}}/></label>
|
||||
</div>
|
||||
{{if .IsAdmin}}
|
||||
<div>
|
||||
<label for="field_sso">SSO: <input type="checkbox" name="sso" id="field_sso" {{if .EditApp.Sso}}checked{{end}}/></label>
|
||||
</div>
|
||||
{{end}}
|
||||
<div>
|
||||
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active" {{if .EditApp.Active}}checked{{end}}/></label>
|
||||
</div>
|
||||
<button type="submit">Edit</button>
|
||||
</form>
|
||||
<form method="GET" action="/manage/apps">
|
||||
<input type="hidden" name="offset" value="{{.Offset}}"/>
|
||||
<button type="submit">Cancel</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
93
web/src/pages/manage-apps.go.html
Normal file
93
web/src/pages/manage-apps.go.html
Normal file
@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
<script>
|
||||
window.addEventListener("load", function () {
|
||||
selectText("app-secret");
|
||||
});
|
||||
|
||||
// Thanks again: https://stackoverflow.com/a/987376
|
||||
function selectText(nodeId) {
|
||||
const node = document.getElementById(nodeId);
|
||||
|
||||
if (document.body.createTextRange) {
|
||||
const range = document.body.createTextRange();
|
||||
range.moveToElementText(node);
|
||||
range.select();
|
||||
} else if (window.getSelection) {
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
} else {
|
||||
console.warn("Could not select text in node: Unsupported browser.");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{{template "header.go.html" .}}
|
||||
<main>
|
||||
<form method="GET" action="/">
|
||||
<button type="submit">Home</button>
|
||||
</form>
|
||||
|
||||
{{if .NewAppSecret}}
|
||||
<div>New application secret: <span id="app-secret">{{.NewAppSecret}}</span> for {{.NewAppName}}</div>
|
||||
{{end}}
|
||||
|
||||
<h2>Manage Client Applications</h2>
|
||||
<form method="GET" action="/manage/apps/create">
|
||||
<button type="submit">New Client Application</button>
|
||||
</form>
|
||||
{{if eq (len .Apps) 0}}
|
||||
<div>No client applications found</div>
|
||||
{{else}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Domain</th>
|
||||
<th>Perms</th>
|
||||
<th>SSO</th>
|
||||
<th>Active</th>
|
||||
<th>Owner</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Apps}}
|
||||
<tr>
|
||||
<td>{{.Subject}}</td>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{.Domain}}</td>
|
||||
<td>{{.Perms}}</td>
|
||||
<td>{{.Sso}}</td>
|
||||
<td>{{.Active}}</td>
|
||||
<td>{{.Owner}}</td>
|
||||
<td>
|
||||
<form method="GET" action="/manage/apps">
|
||||
<input type="hidden" name="offset" value="{{$.Offset}}"/>
|
||||
<input type="hidden" name="edit" value="{{.Subject}}"/>
|
||||
<button type="submit">Edit</button>
|
||||
</form>
|
||||
<form method="POST" action="/manage/apps?offset={{$.Offset}}">
|
||||
<input type="hidden" name="action" value="secret"/>
|
||||
<input type="hidden" name="offset" value="{{$.Offset}}"/>
|
||||
<input type="hidden" name="subject" value="{{.Subject}}"/>
|
||||
<button type="submit">Reset Secret</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
46
web/src/pages/manage-users-create.go.html
Normal file
46
web/src/pages/manage-users-create.go.html
Normal file
@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<link rel="stylesheet" href="/theme/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>{{.ServiceName}}</h1>
|
||||
</header>
|
||||
<main>
|
||||
<form method="GET" action="/">
|
||||
<button type="submit">Home</button>
|
||||
</form>
|
||||
|
||||
<h2>Create User</h2>
|
||||
<form method="POST" action="/manage/users">
|
||||
<input type="hidden" name="action" value="create"/>
|
||||
<input type="hidden" name="offset" value="{{.Offset}}"/>
|
||||
<div>
|
||||
<label for="field_name">Name:</label>
|
||||
<input type="text" name="name" id="field_name" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_username">Username:</label>
|
||||
<input type="text" name="username" id="field_username" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_email">Email:</label>
|
||||
<p>Using an `@{{.Namespace}}` email address will automatically verify as it is owned by this login
|
||||
service.</p>
|
||||
<input type="text" name="email" id="field_email" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_role">Roles:</label>
|
||||
<input type="text" name="roles" id="field_roles" value="{{.EditUser.Roles}}" size="100"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active"
|
||||
checked/></label>
|
||||
</div>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
38
web/src/pages/manage-users-edit.go.html
Normal file
38
web/src/pages/manage-users-edit.go.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
</head>
|
||||
<body>
|
||||
{{template "header.go.html" .}}
|
||||
<main>
|
||||
<form method="GET" action="/">
|
||||
<button type="submit">Home</button>
|
||||
</form>
|
||||
|
||||
<h2>Edit User</h2>
|
||||
<form method="POST" action="/manage/users">
|
||||
<input type="hidden" name="action" value="edit"/>
|
||||
<input type="hidden" name="offset" value="{{.Offset}}"/>
|
||||
<div>
|
||||
<label for="field_subject">Subject:</label>
|
||||
<input type="text" name="subject" id="field_subject" value="{{.EditUser.Subject}}" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_roles">Roles:</label>
|
||||
<input type="text" name="roles" id="field_roles" value="{{.EditUser.Roles}}" size="100"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active" checked/></label>
|
||||
</div>
|
||||
<button type="submit">Edit</button>
|
||||
</form>
|
||||
<form method="GET" action="/manage/users">
|
||||
<input type="hidden" name="offset" value="{{.Offset}}"/>
|
||||
<button type="submit">Cancel</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
71
web/src/pages/manage-users.go.html
Normal file
71
web/src/pages/manage-users.go.html
Normal file
@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
</head>
|
||||
<body>
|
||||
{{template "header.go.html" .}}
|
||||
<main>
|
||||
<form method="GET" action="/">
|
||||
<button type="submit">Home</button>
|
||||
</form>
|
||||
|
||||
<h2>Manage Users</h2>
|
||||
{{if eq (len .Users) 0}}
|
||||
<div>No users found, this is definitely a bug.</div>
|
||||
{{else}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Subject</th>
|
||||
<th>Email</th>
|
||||
<th>Email Verified</th>
|
||||
<th>Roles</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Active</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Users}}
|
||||
<tr>
|
||||
<td>{{.Subject}}</td>
|
||||
<th>
|
||||
{{if $.EmailShow}}
|
||||
<span>{{.Email}}</span>
|
||||
{{else}}
|
||||
<span>{{emailHide .Email}}</span>
|
||||
{{end}}
|
||||
</th>
|
||||
<th>{{.EmailVerified}}</th>
|
||||
<th>{{.Roles}}</th>
|
||||
<th>{{.UpdatedAt}}</th>
|
||||
<td>{{.Active}}</td>
|
||||
<td>
|
||||
<form method="GET" action="/manage/users">
|
||||
<input type="hidden" name="offset" value="{{$.Offset}}"/>
|
||||
<input type="hidden" name="edit" value="{{.Subject}}"/>
|
||||
<button type="submit">Edit</button>
|
||||
</form>
|
||||
<form method="POST" action="/reset-password">
|
||||
<input type="hidden" name="email" value="{{.Email}}"/>
|
||||
<button type="submit">Send Reset Password Email</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<form method="GET" action="/manage/users">
|
||||
<input type="hidden" name="offset" value="{{.Offset}}"/>
|
||||
{{if not .EmailShow}}
|
||||
<input type="hidden" name="show-email"/>
|
||||
{{end}}
|
||||
<button type="submit">{{if .EmailShow}}Hide Email Addresses{{else}}Show email addresses{{end}}</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
35
web/src/pages/oauth-authorize.go.html
Normal file
35
web/src/pages/oauth-authorize.go.html
Normal file
@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
</head>
|
||||
<body>
|
||||
{{template "header.go.html" .}}
|
||||
<main>
|
||||
<form method="POST" action="/authorize">
|
||||
<div>The application {{.AppName}} wants to access your account ({{.Auth.UserInfo.name}}). It requests the following permissions:</div>
|
||||
<div>
|
||||
<ul>
|
||||
{{range .WantsList}}
|
||||
<li>{{.}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<input type="hidden" name="response_type" value="{{.ResponseType}}"/>
|
||||
<input type="hidden" name="response_mode" value="{{.ResponseMode}}">
|
||||
<input type="hidden" name="client_id" value="{{.ClientID}}"/>
|
||||
<input type="hidden" name="redirect_uri" value="{{.RedirectUri}}"/>
|
||||
<input type="hidden" name="state" value="{{.State}}"/>
|
||||
<input type="hidden" name="scope" value="{{.Scope}}"/>
|
||||
<input type="hidden" name="nonce" value="{{.Nonce}}"/>
|
||||
<button class="oauth-action-authorize" name="oauth_action" value="authorize">Authorize</button>
|
||||
<button class="oauth-action-cancel" name="oauth_action" value="cancel">Cancel</button>
|
||||
</div>
|
||||
<div>Authorizing this action will redirect you to {{.AppDomain}} with access to the permissions requested above.</div>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
22
web/src/pages/remove-otp.go.html
Normal file
22
web/src/pages/remove-otp.go.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<link rel="stylesheet" href="/theme/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>{{.ServiceName}}</h1>
|
||||
</header>
|
||||
<main>
|
||||
<form method="POST" action="/edit/otp">
|
||||
<input type="hidden" name="remove" value="1"/>
|
||||
<div>
|
||||
<label for="field_code">OTP Code:</label>
|
||||
<input type="text" name="code" id="field_code" required autofocus pattern="[0-9]{6,8}" title="6/7/8 digit one time passcode"/>
|
||||
</div>
|
||||
<button type="submit">Remove OTP</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
26
web/src/pages/reset-password.go.html
Normal file
26
web/src/pages/reset-password.go.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
<link rel="stylesheet" href="/theme/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>{{.ServiceName}}</h1>
|
||||
</header>
|
||||
<main>
|
||||
<form method="POST" action="/mail/password">
|
||||
<input type="hidden" name="code" value="{{.Code}}"/>
|
||||
<div>
|
||||
<label for="field_new_password">New Password:</label>
|
||||
<input type="password" name="new_password" id="field_new_password" autocomplete="new_password" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="field_confirm_password">Confirm Password:</label>
|
||||
<input type="password" name="confirm_password" id="field_confirm_password" autocomplete="confirm_password" required/>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user