mirror of
https://github.com/1f349/tulip.git
synced 2024-12-22 16:24:10 +00:00
Pass config directly to server
This commit is contained in:
parent
9d9d982d7c
commit
96df1deadf
@ -55,7 +55,7 @@ func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...any) subcomm
|
|||||||
return subcommands.ExitFailure
|
return subcommands.ExitFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
var config startUpConfig
|
var config server.Conf
|
||||||
err = json.NewDecoder(openConf).Decode(&config)
|
err = json.NewDecoder(openConf).Decode(&config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("[Tulip] Error: invalid config file: ", err)
|
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
|
return subcommands.ExitSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalLoad(startUp startUpConfig, wd string) {
|
func normalLoad(startUp server.Conf, wd string) {
|
||||||
key := genHmacKey()
|
key := genHmacKey()
|
||||||
|
|
||||||
db, err := database.Open(filepath.Join(wd, "tulip.db.sqlite"))
|
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)
|
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)
|
log.Printf("[Tulip] Starting HTTP server on '%s'\n", srv.Addr)
|
||||||
go utils.RunBackgroundHttp("HTTP", srv)
|
go utils.RunBackgroundHttp("HTTP", srv)
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ func checkDbHasUser(db *database.DB) error {
|
|||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
if err := tx.HasUser(); err != nil {
|
if err := tx.HasUser(); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to add user: %w", err)
|
return fmt.Errorf("failed to add user: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS users
|
|||||||
locale TEXT DEFAULT "en-US" NOT NULL,
|
locale TEXT DEFAULT "en-US" NOT NULL,
|
||||||
role INTEGER DEFAULT 0 NOT NULL,
|
role INTEGER DEFAULT 0 NOT NULL,
|
||||||
updated_at DATETIME,
|
updated_at DATETIME,
|
||||||
|
registered INTEGER DEFAULT 0,
|
||||||
active INTEGER DEFAULT 1
|
active INTEGER DEFAULT 1
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -37,13 +37,13 @@ func (t *Tx) HasUser() error {
|
|||||||
return nil
|
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)
|
pwHash, err := password.HashPassword(pw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return uuid.UUID{}, err
|
return uuid.UUID{}, err
|
||||||
}
|
}
|
||||||
u := uuid.New()
|
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
|
return u, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,18 +235,18 @@ func (t *Tx) InsertClientApp(name, domain string, sso, active bool, owner uuid.U
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tx) UpdateClientApp(subject uuid.UUID, name, domain string, sso, active bool) error {
|
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 = ?`, name, domain, sso, active, subject.String())
|
_, 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
|
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)
|
secret, err := password.GenerateApiSecret(70)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
_, err = t.tx.Exec(`UPDATE client_store SET secret = ? WHERE subject = ?`, secret, subject.String())
|
_, err = t.tx.Exec(`UPDATE client_store SET secret = ? WHERE subject = ? AND owner = ?`, secret, subject.String(), owner.String())
|
||||||
return err
|
return secret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tx) GetUserList(offset int) ([]User, error) {
|
func (t *Tx) GetUserList(offset int) ([]User, error) {
|
||||||
|
@ -2,6 +2,30 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{.ServiceName}}</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
@ -12,6 +36,10 @@
|
|||||||
<button type="submit">Home</button>
|
<button type="submit">Home</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{{if .NewAppSecret}}
|
||||||
|
<div>New application secret: <span id="app-secret">{{.NewAppSecret}}</span> for {{.NewAppName}}</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{if .Edit}}
|
{{if .Edit}}
|
||||||
<h2>Edit Client Application</h2>
|
<h2>Edit Client Application</h2>
|
||||||
<form method="POST" action="/manage/apps">
|
<form method="POST" action="/manage/apps">
|
||||||
@ -31,11 +59,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{{if .IsAdmin}}
|
{{if .IsAdmin}}
|
||||||
<div>
|
<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>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div>
|
<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>
|
</div>
|
||||||
<button type="submit">Edit</button>
|
<button type="submit">Edit</button>
|
||||||
</form>
|
</form>
|
||||||
@ -75,6 +105,12 @@
|
|||||||
<input type="hidden" name="edit" value="{{.Sub}}"/>
|
<input type="hidden" name="edit" value="{{.Sub}}"/>
|
||||||
<button type="submit">Edit</button>
|
<button type="submit">Edit</button>
|
||||||
</form>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -100,7 +136,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<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>
|
</div>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -37,7 +37,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
<button type="submit">Edit</button>
|
<button type="submit">Edit</button>
|
||||||
</form>
|
</form>
|
||||||
@ -98,6 +99,10 @@
|
|||||||
<input type="hidden" name="edit" value="{{.Sub}}"/>
|
<input type="hidden" name="edit" value="{{.Sub}}"/>
|
||||||
<button type="submit">Edit</button>
|
<button type="submit">Edit</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="POST" action="/reset-password">
|
||||||
|
<input type="hidden" name="email" value="{{.Email}}"/>
|
||||||
|
<button type="submit">Send Reset Password Email</button>
|
||||||
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -127,6 +132,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="field_email">Email:</label>
|
<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/>
|
<input type="text" name="email" id="field_email" required/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -137,7 +144,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package main
|
package server
|
||||||
|
|
||||||
import "github.com/1f349/tulip/mail"
|
import "github.com/1f349/tulip/mail"
|
||||||
|
|
||||||
type startUpConfig struct {
|
type Conf struct {
|
||||||
Listen string `json:"listen"`
|
Listen string `json:"listen"`
|
||||||
BaseUrl string `json:"base_url"`
|
BaseUrl string `json:"base_url"`
|
||||||
OtpIssuer string `json:"otp_issuer"`
|
OtpIssuer string `json:"otp_issuer"`
|
||||||
ServiceName string `json:"service_name"`
|
ServiceName string `json:"service_name"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
Mail mail.Mail `json:"mail"`
|
Mail mail.Mail `json:"mail"`
|
||||||
}
|
}
|
@ -31,7 +31,7 @@ func (h *HttpServer) EditGet(rw http.ResponseWriter, _ *http.Request, _ httprout
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
pages.RenderPageTemplate(rw, "edit", map[string]any{
|
pages.RenderPageTemplate(rw, "edit", map[string]any{
|
||||||
"ServiceName": h.serviceName,
|
"ServiceName": h.conf.ServiceName,
|
||||||
"User": user,
|
"User": user,
|
||||||
"Nonce": lNonce,
|
"Nonce": lNonce,
|
||||||
"FieldPronoun": user.Pronouns.String(),
|
"FieldPronoun": user.Pronouns.String(),
|
||||||
|
@ -14,7 +14,7 @@ func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httproute
|
|||||||
rw.WriteHeader(http.StatusOK)
|
rw.WriteHeader(http.StatusOK)
|
||||||
if auth.IsGuest() {
|
if auth.IsGuest() {
|
||||||
pages.RenderPageTemplate(rw, "index-guest", map[string]any{
|
pages.RenderPageTemplate(rw, "index-guest", map[string]any{
|
||||||
"ServiceName": h.serviceName,
|
"ServiceName": h.conf.ServiceName,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -37,7 +37,7 @@ func (h *HttpServer) Home(rw http.ResponseWriter, req *http.Request, _ httproute
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
pages.RenderPageTemplate(rw, "index", map[string]any{
|
pages.RenderPageTemplate(rw, "index", map[string]any{
|
||||||
"ServiceName": h.serviceName,
|
"ServiceName": h.conf.ServiceName,
|
||||||
"Auth": auth,
|
"Auth": auth,
|
||||||
"User": userWithName,
|
"User": userWithName,
|
||||||
"Nonce": lNonce,
|
"Nonce": lNonce,
|
||||||
|
@ -43,7 +43,7 @@ func (h *HttpServer) LoginGet(rw http.ResponseWriter, req *http.Request, _ httpr
|
|||||||
rw.Header().Set("Content-Type", "text/html")
|
rw.Header().Set("Content-Type", "text/html")
|
||||||
rw.WriteHeader(http.StatusOK)
|
rw.WriteHeader(http.StatusOK)
|
||||||
pages.RenderPageTemplate(rw, "login", map[string]any{
|
pages.RenderPageTemplate(rw, "login", map[string]any{
|
||||||
"ServiceName": h.serviceName,
|
"ServiceName": h.conf.ServiceName,
|
||||||
"Redirect": req.URL.Query().Get("redirect"),
|
"Redirect": req.URL.Query().Get("redirect"),
|
||||||
"Mismatch": req.URL.Query().Get("mismatch"),
|
"Mismatch": req.URL.Query().Get("mismatch"),
|
||||||
"LoginName": loginName,
|
"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))
|
h.mailLinkCache.Set(mailLinkKey{mailLinkVerifyEmail, u}, userInfo.Sub, time.Now().Add(10*time.Minute))
|
||||||
|
|
||||||
// try to send email
|
// try to send email
|
||||||
err = h.mailer.SendEmailTemplate("mail-verify", "Verify Email", userInfo.Name, address, map[string]any{
|
err = h.conf.Mail.SendEmailTemplate("mail-verify", "Verify Email", userInfo.Name, address, map[string]any{
|
||||||
"VerifyUrl": h.domain + "/mail/verify/" + u.String(),
|
"VerifyUrl": h.conf.BaseUrl + "/mail/verify/" + u.String(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("[Tulip] Login: Failed to send verification email:", err)
|
log.Println("[Tulip] Login: Failed to send verification email:", err)
|
||||||
|
@ -68,7 +68,7 @@ func (h *HttpServer) MailPassword(rw http.ResponseWriter, req *http.Request, par
|
|||||||
}
|
}
|
||||||
|
|
||||||
pages.RenderPageTemplate(rw, "reset-password", map[string]any{
|
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
|
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 {
|
if err != nil {
|
||||||
http.Error(rw, "Failed to send confirmation email.", http.StatusInternalServerError)
|
http.Error(rw, "Failed to send confirmation email.", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -3,6 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"github.com/1f349/tulip/database"
|
"github.com/1f349/tulip/database"
|
||||||
"github.com/1f349/tulip/pages"
|
"github.com/1f349/tulip/pages"
|
||||||
|
"github.com/go-oauth2/oauth2/v4"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -36,10 +37,12 @@ func (h *HttpServer) ManageAppsGet(rw http.ResponseWriter, req *http.Request, _
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := map[string]any{
|
m := map[string]any{
|
||||||
"ServiceName": h.serviceName,
|
"ServiceName": h.conf.ServiceName,
|
||||||
"Apps": appList,
|
"Apps": appList,
|
||||||
"Offset": offset,
|
"Offset": offset,
|
||||||
"IsAdmin": role == database.RoleAdmin,
|
"IsAdmin": role == database.RoleAdmin,
|
||||||
|
"NewAppName": q.Get("NewAppName"),
|
||||||
|
"NewAppSecret": q.Get("NewAppSecret"),
|
||||||
}
|
}
|
||||||
if q.Has("edit") {
|
if q.Has("edit") {
|
||||||
for _, i := range appList {
|
for _, i := range appList {
|
||||||
@ -99,10 +102,43 @@ func (h *HttpServer) ManageAppsPost(rw http.ResponseWriter, req *http.Request, _
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return tx.UpdateClientApp(sub, name, domain, sso, active)
|
return tx.UpdateClientApp(sub, auth.Data.ID, name, domain, sso, active)
|
||||||
}) {
|
}) {
|
||||||
return
|
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:
|
default:
|
||||||
http.Error(rw, "400 Bad Request: Invalid action", http.StatusBadRequest)
|
http.Error(rw, "400 Bad Request: Invalid action", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -44,11 +45,12 @@ func (h *HttpServer) ManageUsersGet(rw http.ResponseWriter, req *http.Request, _
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := map[string]any{
|
m := map[string]any{
|
||||||
"ServiceName": h.serviceName,
|
"ServiceName": h.conf.ServiceName,
|
||||||
"Users": userList,
|
"Users": userList,
|
||||||
"Offset": offset,
|
"Offset": offset,
|
||||||
"EmailShow": req.URL.Query().Has("show-email"),
|
"EmailShow": req.URL.Query().Has("show-email"),
|
||||||
"CurrentAdmin": auth.Data.ID,
|
"CurrentAdmin": auth.Data.ID,
|
||||||
|
"Namespace": h.conf.Namespace,
|
||||||
}
|
}
|
||||||
if q.Has("edit") {
|
if q.Has("edit") {
|
||||||
for _, i := range userList {
|
for _, i := range userList {
|
||||||
@ -100,28 +102,33 @@ func (h *HttpServer) ManageUsersPost(rw http.ResponseWriter, req *http.Request,
|
|||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case "create":
|
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
|
// parse email for headers
|
||||||
address, err := mail.ParseAddress(email)
|
address, err := mail.ParseAddress(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(rw, "500 Internal Server Error: Failed to parse user email address", http.StatusInternalServerError)
|
http.Error(rw, "500 Internal Server Error: Failed to parse user email address", http.StatusInternalServerError)
|
||||||
return
|
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()
|
u, u2 := uuid.New(), uuid.New()
|
||||||
h.mailLinkCache.Set(mailLinkKey{mailLinkResetPassword, u}, userSub, time.Now().Add(10*time.Minute))
|
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))
|
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{
|
err = h.conf.Mail.SendEmailTemplate("mail-register-admin", "Register", name, address, map[string]any{
|
||||||
"ResetUrl": h.domain + "/mail/password/" + u.String(),
|
"RegisterUrl": h.conf.BaseUrl + "/mail/password/" + u.String(),
|
||||||
"DeleteUrl": h.domain + "/mail/delete/" + u2.String(),
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("[Tulip] Login: Failed to send register email:", err)
|
log.Println("[Tulip] Login: Failed to send register email:", err)
|
||||||
|
@ -95,7 +95,7 @@ func (h *HttpServer) authorizeEndpoint(rw http.ResponseWriter, req *http.Request
|
|||||||
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
rw.WriteHeader(http.StatusOK)
|
||||||
pages.RenderPageTemplate(rw, "oauth-authorize", map[string]any{
|
pages.RenderPageTemplate(rw, "oauth-authorize", map[string]any{
|
||||||
"ServiceName": h.serviceName,
|
"ServiceName": h.conf.ServiceName,
|
||||||
"AppName": appName,
|
"AppName": appName,
|
||||||
"AppDomain": appDomain,
|
"AppDomain": appDomain,
|
||||||
"User": user,
|
"User": user,
|
||||||
|
@ -19,7 +19,7 @@ func (h *HttpServer) LoginOtpGet(rw http.ResponseWriter, req *http.Request, _ ht
|
|||||||
}
|
}
|
||||||
|
|
||||||
pages.RenderPageTemplate(rw, "login-otp", map[string]any{
|
pages.RenderPageTemplate(rw, "login-otp", map[string]any{
|
||||||
"ServiceName": h.serviceName,
|
"ServiceName": h.conf.ServiceName,
|
||||||
"Redirect": req.URL.Query().Get("redirect"),
|
"Redirect": req.URL.Query().Get("redirect"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@ func (h *HttpServer) fetchAndValidateOtp(rw http.ResponseWriter, sub uuid.UUID,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if hasOtp {
|
if hasOtp {
|
||||||
otp, err = tx.GetTwoFactor(sub, h.otpIssuer)
|
otp, err = tx.GetTwoFactor(sub, h.conf.OtpIssuer)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}) {
|
}) {
|
||||||
@ -121,7 +121,7 @@ func (h *HttpServer) EditOtpGet(rw http.ResponseWriter, req *http.Request, _ htt
|
|||||||
|
|
||||||
// generate OTP key
|
// generate OTP key
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
http.Error(rw, "500 Internal Server Error: Failed to generate OTP key", http.StatusInternalServerError)
|
http.Error(rw, "500 Internal Server Error: Failed to generate OTP key", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -150,7 +150,7 @@ func (h *HttpServer) EditOtpGet(rw http.ResponseWriter, req *http.Request, _ htt
|
|||||||
|
|
||||||
// render page
|
// render page
|
||||||
pages.RenderPageTemplate(rw, "edit-otp", map[string]any{
|
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)),
|
"OtpQr": template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(otpQr)),
|
||||||
"OtpUrl": otpUrl,
|
"OtpUrl": otpUrl,
|
||||||
})
|
})
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/1f349/cache"
|
"github.com/1f349/cache"
|
||||||
clientStore "github.com/1f349/tulip/client-store"
|
clientStore "github.com/1f349/tulip/client-store"
|
||||||
"github.com/1f349/tulip/database"
|
"github.com/1f349/tulip/database"
|
||||||
"github.com/1f349/tulip/mail"
|
|
||||||
"github.com/1f349/tulip/openid"
|
"github.com/1f349/tulip/openid"
|
||||||
scope2 "github.com/1f349/tulip/scope"
|
scope2 "github.com/1f349/tulip/scope"
|
||||||
"github.com/go-oauth2/oauth2/v4/errors"
|
"github.com/go-oauth2/oauth2/v4/errors"
|
||||||
@ -32,11 +31,8 @@ type HttpServer struct {
|
|||||||
oauthSrv *server.Server
|
oauthSrv *server.Server
|
||||||
oauthMgr *manage.Manager
|
oauthMgr *manage.Manager
|
||||||
db *database.DB
|
db *database.DB
|
||||||
domain string
|
conf Conf
|
||||||
privKey []byte
|
privKey []byte
|
||||||
otpIssuer string
|
|
||||||
serviceName string
|
|
||||||
mailer mail.Mail
|
|
||||||
|
|
||||||
// mailLinkCache contains a mapping of verify uuids to user uuids
|
// mailLinkCache contains a mapping of verify uuids to user uuids
|
||||||
mailLinkCache *cache.Cache[mailLinkKey, uuid.UUID]
|
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)
|
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()
|
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)
|
openIdBytes, err := json.Marshal(openIdConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Failed to generate OpenID configuration:", err)
|
log.Fatalln("Failed to generate OpenID configuration:", err)
|
||||||
@ -87,11 +91,8 @@ func NewHttpServer(listen, domain, otpIssuer, serviceName string, mailer mail.Ma
|
|||||||
oauthSrv: oauthSrv,
|
oauthSrv: oauthSrv,
|
||||||
oauthMgr: oauthManager,
|
oauthMgr: oauthManager,
|
||||||
db: db,
|
db: db,
|
||||||
domain: domain,
|
conf: conf,
|
||||||
privKey: privKey,
|
privKey: privKey,
|
||||||
otpIssuer: otpIssuer,
|
|
||||||
serviceName: serviceName,
|
|
||||||
mailer: mailer,
|
|
||||||
|
|
||||||
mailLinkCache: cache.New[mailLinkKey, uuid.UUID](),
|
mailLinkCache: cache.New[mailLinkKey, uuid.UUID](),
|
||||||
}
|
}
|
||||||
@ -220,10 +221,10 @@ func NewHttpServer(listen, domain, otpIssuer, serviceName string, mailer mail.Ma
|
|||||||
m["name"] = userData.Name
|
m["name"] = userData.Name
|
||||||
}
|
}
|
||||||
if claims["username"] {
|
if claims["username"] {
|
||||||
m["preferred_username"] = userData.Name
|
m["preferred_username"] = userData.Username
|
||||||
}
|
}
|
||||||
if claims["profile"] {
|
if claims["profile"] {
|
||||||
m["profile"] = domain + "/user/" + userData.Username
|
m["profile"] = conf.BaseUrl + "/user/" + userData.Username
|
||||||
m["picture"] = userData.Picture.String()
|
m["picture"] = userData.Picture.String()
|
||||||
m["website"] = userData.Website.String()
|
m["website"] = userData.Website.String()
|
||||||
}
|
}
|
||||||
@ -249,7 +250,7 @@ func NewHttpServer(listen, domain, otpIssuer, serviceName string, mailer mail.Ma
|
|||||||
})
|
})
|
||||||
|
|
||||||
return &http.Server{
|
return &http.Server{
|
||||||
Addr: listen,
|
Addr: conf.Listen,
|
||||||
Handler: r,
|
Handler: r,
|
||||||
ReadTimeout: time.Minute,
|
ReadTimeout: time.Minute,
|
||||||
ReadHeaderTimeout: time.Minute,
|
ReadHeaderTimeout: time.Minute,
|
||||||
|
Loading…
Reference in New Issue
Block a user