Pass config directly to server

This commit is contained in:
Melon 2023-10-10 18:06:43 +01:00
parent 9d9d982d7c
commit 96df1deadf
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
15 changed files with 164 additions and 73 deletions

View File

@ -55,7 +55,7 @@ func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...any) subcomm
return subcommands.ExitFailure
}
var config startUpConfig
var config server.Conf
err = json.NewDecoder(openConf).Decode(&config)
if err != nil {
log.Println("[Tulip] Error: invalid config file: ", err)
@ -71,7 +71,7 @@ func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...any) subcomm
return subcommands.ExitSuccess
}
func normalLoad(startUp startUpConfig, wd string) {
func normalLoad(startUp server.Conf, wd string) {
key := genHmacKey()
db, err := database.Open(filepath.Join(wd, "tulip.db.sqlite"))
@ -91,7 +91,7 @@ func normalLoad(startUp startUpConfig, wd string) {
log.Fatal("[Tulip] Failed to load mail templates:", err)
}
srv := server.NewHttpServer(startUp.Listen, startUp.BaseUrl, startUp.OtpIssuer, startUp.ServiceName, startUp.Mail, db, key)
srv := server.NewHttpServer(startUp, db, key)
log.Printf("[Tulip] Starting HTTP server on '%s'\n", srv.Addr)
go utils.RunBackgroundHttp("HTTP", srv)
@ -122,7 +122,7 @@ func checkDbHasUser(db *database.DB) error {
defer tx.Rollback()
if err := tx.HasUser(); err != nil {
if errors.Is(err, sql.ErrNoRows) {
_, err := tx.InsertUser("Admin", "admin", "admin", "admin@localhost", database.RoleAdmin, false)
_, err := tx.InsertUser("Admin", "admin", "admin", "admin@localhost", false, database.RoleAdmin, false)
if err != nil {
return fmt.Errorf("failed to add user: %w", err)
}

View File

@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS users
locale TEXT DEFAULT "en-US" NOT NULL,
role INTEGER DEFAULT 0 NOT NULL,
updated_at DATETIME,
registered INTEGER DEFAULT 0,
active INTEGER DEFAULT 1
);

View File

@ -37,13 +37,13 @@ func (t *Tx) HasUser() error {
return nil
}
func (t *Tx) InsertUser(name, un, pw, email string, role UserRole, active bool) (uuid.UUID, error) {
func (t *Tx) InsertUser(name, un, pw, email string, verifyEmail bool, role UserRole, active bool) (uuid.UUID, error) {
pwHash, err := password.HashPassword(pw)
if err != nil {
return uuid.UUID{}, err
}
u := uuid.New()
_, err = t.tx.Exec(`INSERT INTO users (subject, name, username, password, email, role, updated_at, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, u, name, un, pwHash, email, role, updatedAt(), active)
_, err = t.tx.Exec(`INSERT INTO users (subject, name, username, password, email, email_verified, role, updated_at, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, u, name, un, pwHash, email, verifyEmail, role, updatedAt(), active)
return u, err
}
@ -235,18 +235,18 @@ func (t *Tx) InsertClientApp(name, domain string, sso, active bool, owner uuid.U
return err
}
func (t *Tx) UpdateClientApp(subject uuid.UUID, name, domain string, sso, active bool) error {
_, err := t.tx.Exec(`UPDATE client_store SET name = ?, domain = ?, sso = ?, active = ? WHERE subject = ?`, name, domain, sso, active, subject.String())
func (t *Tx) UpdateClientApp(subject, owner uuid.UUID, name, domain string, sso, active bool) error {
_, err := t.tx.Exec(`UPDATE client_store SET name = ?, domain = ?, sso = ?, active = ? WHERE subject = ? AND owner = ?`, name, domain, sso, active, subject.String(), owner.String())
return err
}
func (t *Tx) ResetClientAppSecret(subject uuid.UUID, secret string) error {
func (t *Tx) ResetClientAppSecret(subject, owner uuid.UUID) (string, error) {
secret, err := password.GenerateApiSecret(70)
if err != nil {
return err
return "", err
}
_, err = t.tx.Exec(`UPDATE client_store SET secret = ? WHERE subject = ?`, secret, subject.String())
return err
_, err = t.tx.Exec(`UPDATE client_store SET secret = ? WHERE subject = ? AND owner = ?`, secret, subject.String(), owner.String())
return secret, err
}
func (t *Tx) GetUserList(offset int) ([]User, error) {

View File

@ -2,6 +2,30 @@
<html lang="en">
<head>
<title>{{.ServiceName}}</title>
<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>
<header>
@ -12,6 +36,10 @@
<button type="submit">Home</button>
</form>
{{if .NewAppSecret}}
<div>New application secret: <span id="app-secret">{{.NewAppSecret}}</span> for {{.NewAppName}}</div>
{{end}}
{{if .Edit}}
<h2>Edit Client Application</h2>
<form method="POST" action="/manage/apps">
@ -31,11 +59,13 @@
</div>
{{if .IsAdmin}}
<div>
<label for="field_sso">SSO: <input type="checkbox" name="sso" id="field_sso" {{if .Edit.SSO}}checked{{end}}/></label>
<label for="field_sso">SSO: <input type="checkbox" name="sso" id="field_sso"
{{if .Edit.SSO}}checked{{end}}/></label>
</div>
{{end}}
<div>
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active" {{if .Edit.Active}}checked{{end}}/></label>
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active"
{{if .Edit.Active}}checked{{end}}/></label>
</div>
<button type="submit">Edit</button>
</form>
@ -75,6 +105,12 @@
<input type="hidden" name="edit" value="{{.Sub}}"/>
<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="{{.Sub}}"/>
<button type="submit">Reset Secret</button>
</form>
</td>
</tr>
{{end}}
@ -100,7 +136,8 @@
</div>
{{end}}
<div>
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active" checked/></label>
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active"
checked/></label>
</div>
<button type="submit">Create</button>
</form>

View File

@ -37,7 +37,8 @@
</select>
</div>
<div>
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active" checked/></label>
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active"
checked/></label>
</div>
<button type="submit">Edit</button>
</form>
@ -98,6 +99,10 @@
<input type="hidden" name="edit" value="{{.Sub}}"/>
<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>
{{end}}
</td>
</tr>
@ -127,6 +132,8 @@
</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>
@ -137,7 +144,8 @@
</select>
</div>
<div>
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active" checked/></label>
<label for="field_active">Active: <input type="checkbox" name="active" id="field_active"
checked/></label>
</div>
<button type="submit">Create</button>
</form>

View File

@ -1,11 +1,12 @@
package main
package server
import "github.com/1f349/tulip/mail"
type startUpConfig struct {
type Conf struct {
Listen string `json:"listen"`
BaseUrl string `json:"base_url"`
OtpIssuer string `json:"otp_issuer"`
ServiceName string `json:"service_name"`
Namespace string `json:"namespace"`
Mail mail.Mail `json:"mail"`
}

View File

@ -31,7 +31,7 @@ func (h *HttpServer) EditGet(rw http.ResponseWriter, _ *http.Request, _ httprout
return
}
pages.RenderPageTemplate(rw, "edit", map[string]any{
"ServiceName": h.serviceName,
"ServiceName": h.conf.ServiceName,
"User": user,
"Nonce": lNonce,
"FieldPronoun": user.Pronouns.String(),

View File

@ -14,7 +14,7 @@ func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httproute
rw.WriteHeader(http.StatusOK)
if auth.IsGuest() {
pages.RenderPageTemplate(rw, "index-guest", map[string]any{
"ServiceName": h.serviceName,
"ServiceName": h.conf.ServiceName,
})
return
}
@ -37,7 +37,7 @@ func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httproute
return
}
pages.RenderPageTemplate(rw, "index", map[string]any{
"ServiceName": h.serviceName,
"ServiceName": h.conf.ServiceName,
"Auth": auth,
"User": userWithName,
"Nonce": lNonce,

View File

@ -43,7 +43,7 @@ func (h *HttpServer) LoginGet(rw http.ResponseWriter, req *http.Request, _ httpr
rw.Header().Set("Content-Type", "text/html")
rw.WriteHeader(http.StatusOK)
pages.RenderPageTemplate(rw, "login", map[string]any{
"ServiceName": h.serviceName,
"ServiceName": h.conf.ServiceName,
"Redirect": req.URL.Query().Get("redirect"),
"Mismatch": req.URL.Query().Get("mismatch"),
"LoginName": loginName,
@ -100,8 +100,8 @@ func (h *HttpServer) LoginPost(rw http.ResponseWriter, req *http.Request, _ http
h.mailLinkCache.Set(mailLinkKey{mailLinkVerifyEmail, u}, userInfo.Sub, time.Now().Add(10*time.Minute))
// try to send email
err = h.mailer.SendEmailTemplate("mail-verify", "Verify Email", userInfo.Name, address, map[string]any{
"VerifyUrl": h.domain + "/mail/verify/" + u.String(),
err = h.conf.Mail.SendEmailTemplate("mail-verify", "Verify Email", userInfo.Name, address, map[string]any{
"VerifyUrl": h.conf.BaseUrl + "/mail/verify/" + u.String(),
})
if err != nil {
log.Println("[Tulip] Login: Failed to send verification email:", err)

View File

@ -68,7 +68,7 @@ func (h *HttpServer) MailPassword(rw http.ResponseWriter, req *http.Request, par
}
pages.RenderPageTemplate(rw, "reset-password", map[string]any{
"ServiceName": h.serviceName,
"ServiceName": h.conf.ServiceName,
})
}
@ -155,7 +155,7 @@ func (h *HttpServer) MailDelete(rw http.ResponseWriter, req *http.Request, param
return
}
err = h.mailer.SendEmailTemplate("mail-account-delete", "Account Deletion", userInfo.Name, address, nil)
err = h.conf.Mail.SendEmailTemplate("mail-account-delete", "Account Deletion", userInfo.Name, address, nil)
if err != nil {
http.Error(rw, "Failed to send confirmation email.", http.StatusInternalServerError)
return

View File

@ -3,6 +3,7 @@ package server
import (
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/pages"
"github.com/go-oauth2/oauth2/v4"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
"net/http"
@ -36,10 +37,12 @@ func (h *HttpServer) ManageAppsGet(rw http.ResponseWriter, req *http.Request, _
}
m := map[string]any{
"ServiceName": h.serviceName,
"ServiceName": h.conf.ServiceName,
"Apps": appList,
"Offset": offset,
"IsAdmin": role == database.RoleAdmin,
"NewAppName": q.Get("NewAppName"),
"NewAppSecret": q.Get("NewAppSecret"),
}
if q.Has("edit") {
for _, i := range appList {
@ -99,10 +102,43 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
if err != nil {
return err
}
return tx.UpdateClientApp(sub, name, domain, sso, active)
return tx.UpdateClientApp(sub, auth.Data.ID, name, domain, sso, active)
}) {
return
}
case "secret":
var info oauth2.ClientInfo
var secret string
if h.DbTx(rw, func(tx *database.Tx) error {
sub, err := uuid.Parse(req.Form.Get("subject"))
if err != nil {
return err
}
info, err = tx.GetClientInfo(sub.String())
if err != nil {
return err
}
secret, err = tx.ResetClientAppSecret(sub, auth.Data.ID)
return err
}) {
return
}
appName := "Unknown..."
if getName, ok := info.(interface{ GetName() string }); ok {
appName = getName.GetName()
}
h.ManageAppsGet(rw, &http.Request{
URL: &url.URL{
RawQuery: url.Values{
"offset": []string{offset},
"NewAppName": []string{appName},
"NewAppSecret": []string{secret},
}.Encode(),
},
}, httprouter.Params{}, auth)
return
default:
http.Error(rw, "400 Bad Request: Invalid action", http.StatusBadRequest)
return

View File

@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
@ -44,11 +45,12 @@ func (h *HttpServer) ManageUsersGet(rw http.ResponseWriter, req *http.Request, _
}
m := map[string]any{
"ServiceName": h.serviceName,
"ServiceName": h.conf.ServiceName,
"Users": userList,
"Offset": offset,
"EmailShow": req.URL.Query().Has("show-email"),
"CurrentAdmin": auth.Data.ID,
"Namespace": h.conf.Namespace,
}
if q.Has("edit") {
for _, i := range userList {
@ -100,28 +102,33 @@ func (h *HttpServer) ManageUsersPost(rw http.ResponseWriter, req *http.Request,
switch action {
case "create":
var userSub uuid.UUID
if h.DbTx(rw, func(tx *database.Tx) (err error) {
userSub, err = tx.InsertUser(name, username, "", email, newRole, active)
return err
}) {
return
}
// parse email for headers
address, err := mail.ParseAddress(email)
if err != nil {
http.Error(rw, "500 Internal Server Error: Failed to parse user email address", http.StatusInternalServerError)
return
}
n := strings.IndexByte(address.Address, '@')
// This case should never happen and fail the above address parsing
if n == -1 {
return
}
addrDomain := address.Address[n+1:]
var userSub uuid.UUID
if h.DbTx(rw, func(tx *database.Tx) (err error) {
userSub, err = tx.InsertUser(name, username, "", email, addrDomain == h.conf.Namespace, newRole, active)
return err
}) {
return
}
u, u2 := uuid.New(), uuid.New()
h.mailLinkCache.Set(mailLinkKey{mailLinkResetPassword, u}, userSub, time.Now().Add(10*time.Minute))
h.mailLinkCache.Set(mailLinkKey{mailLinkDelete, u2}, userSub, time.Now().Add(10*time.Minute))
err = h.mailer.SendEmailTemplate("mail-register-delete", "Register", name, address, map[string]any{
"ResetUrl": h.domain + "/mail/password/" + u.String(),
"DeleteUrl": h.domain + "/mail/delete/" + u2.String(),
err = h.conf.Mail.SendEmailTemplate("mail-register-admin", "Register", name, address, map[string]any{
"RegisterUrl": h.conf.BaseUrl + "/mail/password/" + u.String(),
})
if err != nil {
log.Println("[Tulip] Login: Failed to send register email:", err)

View File

@ -95,7 +95,7 @@ func (h *HttpServer) authorizeEndpoint(rw http.ResponseWriter, req *http.Request
rw.WriteHeader(http.StatusOK)
pages.RenderPageTemplate(rw, "oauth-authorize", map[string]any{
"ServiceName": h.serviceName,
"ServiceName": h.conf.ServiceName,
"AppName": appName,
"AppDomain": appDomain,
"User": user,

View File

@ -19,7 +19,7 @@ func (h *HttpServer) LoginOtpGet(rw http.ResponseWriter, req *http.Request, _ ht
}
pages.RenderPageTemplate(rw, "login-otp", map[string]any{
"ServiceName": h.serviceName,
"ServiceName": h.conf.ServiceName,
"Redirect": req.URL.Query().Get("redirect"),
})
}
@ -53,7 +53,7 @@ func (h *HttpServer) fetchAndValidateOtp(rw http.ResponseWriter, sub uuid.UUID,
return
}
if hasOtp {
otp, err = tx.GetTwoFactor(sub, h.otpIssuer)
otp, err = tx.GetTwoFactor(sub, h.conf.OtpIssuer)
}
return
}) {
@ -121,7 +121,7 @@ func (h *HttpServer) EditOtpGet(rw http.ResponseWriter, req *http.Request, _ htt
// generate OTP key
var err error
otp, err = twofactor.NewTOTP(email, h.otpIssuer, crypto.SHA512, digits)
otp, err = twofactor.NewTOTP(email, h.conf.OtpIssuer, crypto.SHA512, digits)
if err != nil {
http.Error(rw, "500 Internal Server Error: Failed to generate OTP key", http.StatusInternalServerError)
return
@ -150,7 +150,7 @@ func (h *HttpServer) EditOtpGet(rw http.ResponseWriter, req *http.Request, _ htt
// render page
pages.RenderPageTemplate(rw, "edit-otp", map[string]any{
"ServiceName": h.serviceName,
"ServiceName": h.conf.ServiceName,
"OtpQr": template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(otpQr)),
"OtpUrl": otpUrl,
})

View File

@ -8,7 +8,6 @@ import (
"github.com/1f349/cache"
clientStore "github.com/1f349/tulip/client-store"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/mail"
"github.com/1f349/tulip/openid"
scope2 "github.com/1f349/tulip/scope"
"github.com/go-oauth2/oauth2/v4/errors"
@ -32,11 +31,8 @@ type HttpServer struct {
oauthSrv *server.Server
oauthMgr *manage.Manager
db *database.DB
domain string
conf Conf
privKey []byte
otpIssuer string
serviceName string
mailer mail.Mail
// mailLinkCache contains a mapping of verify uuids to user uuids
mailLinkCache *cache.Cache[mailLinkKey, uuid.UUID]
@ -71,10 +67,18 @@ func (h *HttpServer) SafeRedirect(rw http.ResponseWriter, req *http.Request) {
http.Redirect(rw, req, parse.String(), http.StatusFound)
}
func NewHttpServer(listen, domain, otpIssuer, serviceName string, mailer mail.Mail, db *database.DB, privKey []byte) *http.Server {
func NewHttpServer(conf Conf, db *database.DB, privKey []byte) *http.Server {
r := httprouter.New()
openIdConf := openid.GenConfig(domain, []string{"openid", "name", "username", "profile", "email", "birthdate", "age", "zoneinfo", "locale"}, []string{"sub", "name", "preferred_username", "profile", "picture", "website", "email", "email_verified", "gender", "birthdate", "zoneinfo", "locale", "updated_at"})
// remove last slash from baseUrl
{
l := len(conf.BaseUrl)
if conf.BaseUrl[l-1] == '/' {
conf.BaseUrl = conf.BaseUrl[:l-1]
}
}
openIdConf := openid.GenConfig(conf.BaseUrl, []string{"openid", "name", "username", "profile", "email", "birthdate", "age", "zoneinfo", "locale"}, []string{"sub", "name", "preferred_username", "profile", "picture", "website", "email", "email_verified", "gender", "birthdate", "zoneinfo", "locale", "updated_at"})
openIdBytes, err := json.Marshal(openIdConf)
if err != nil {
log.Fatalln("Failed to generate OpenID configuration:", err)
@ -87,11 +91,8 @@ func NewHttpServer(listen, domain, otpIssuer, serviceName string, mailer mail.Ma
oauthSrv: oauthSrv,
oauthMgr: oauthManager,
db: db,
domain: domain,
conf: conf,
privKey: privKey,
otpIssuer: otpIssuer,
serviceName: serviceName,
mailer: mailer,
mailLinkCache: cache.New[mailLinkKey, uuid.UUID](),
}
@ -220,10 +221,10 @@ func NewHttpServer(listen, domain, otpIssuer, serviceName string, mailer mail.Ma
m["name"] = userData.Name
}
if claims["username"] {
m["preferred_username"] = userData.Name
m["preferred_username"] = userData.Username
}
if claims["profile"] {
m["profile"] = domain + "/user/" + userData.Username
m["profile"] = conf.BaseUrl + "/user/" + userData.Username
m["picture"] = userData.Picture.String()
m["website"] = userData.Website.String()
}
@ -249,7 +250,7 @@ func NewHttpServer(listen, domain, otpIssuer, serviceName string, mailer mail.Ma
})
return &http.Server{
Addr: listen,
Addr: conf.Listen,
Handler: r,
ReadTimeout: time.Minute,
ReadHeaderTimeout: time.Minute,