Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
Rename updateUser to manageUser Used for both creation and update.
package main
import (
"context"
"embed"
"flag"
"html/template"
"log"
"net"
"net/http"
"time"
"github.com/go-chi/chi/v5"
)
var (
//go:embed template
templateFS embed.FS
//go:embed static
staticFS embed.FS
)
func main() {
var configFilename, listenAddr string
flag.StringVar(&configFilename, "config", "/etc/sinwon/config", "Configuration filename")
flag.StringVar(&listenAddr, "listen", ":8080", "HTTP listen address")
flag.Parse()
cfg, err := loadConfig(configFilename)
if err != nil {
log.Fatalf("Failed to load config file: %v", err)
}
if listenAddr == "" {
listenAddr = cfg.Listen
}
if listenAddr == "" {
log.Fatalf("Missing listen configuration")
}
if cfg.Database == "" {
log.Fatalf("Missing database configuration")
}
db, err := openDB(cfg.Database)
if err != nil {
log.Fatalf("Failed to open DB: %v", err)
}
tpl := template.Must(template.ParseFS(templateFS, "template/*.html"))
mux := chi.NewRouter()
mux.Handle("/static/*", http.FileServer(http.FS(staticFS)))
mux.Get("/", index)
mux.HandleFunc("/login", login)
mux.Post("/logout", logout)
mux.Post("/client/new", createClient)
mux.HandleFunc("/user/new", updateUser)
mux.HandleFunc("/user/{id}", updateUser)
mux.HandleFunc("/user/new", manageUser)
mux.HandleFunc("/user/{id}", manageUser)
mux.Get("/.well-known/oauth-authorization-server", getOAuthServerMetadata)
mux.HandleFunc("/authorize", authorize)
mux.Post("/token", exchangeToken)
go maintainDBLoop(db)
server := http.Server{
Addr: listenAddr,
Handler: loginTokenMiddleware(mux),
BaseContext: func(net.Listener) context.Context {
return newBaseContext(db, tpl)
},
}
log.Printf("OAuth server listening on %v", server.Addr)
if err := server.ListenAndServe(); err != nil {
log.Fatalf("Failed to listen and serve: %v", err)
}
}
func httpError(w http.ResponseWriter, err error) {
log.Print(err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
func maintainDBLoop(db *DB) {
ticker := time.NewTicker(15 * time.Minute)
defer ticker.Stop()
for range ticker.C {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
if err := db.Maintain(ctx); err != nil {
log.Printf("Failed to perform database maintenance: %v", err)
}
cancel()
}
}
{{ template "head.html" }}
<main>
<h1>sinwon</h1>
<form method="post" action="">
Username: <input type="text" name="username" value="{{ .User.Username }}" required><br>
Password: <input type="password" name="password"><br>
{{ if not (eq .Me.ID .User.ID) }}
<label>
<input type="checkbox" name="admin" {{ if .User.Admin }}checked{{ end }}>
Administrator
</label><br>
{{ end }}
<a href="/"><button type="button">Cancel</button></a>
<button type="submit">
{{ if .User.ID }}
Update user
{{ else }}
Create user
{{ end }}
</button>
</form>
</main>
{{ template "foot.html" }}
package main
import (
"fmt"
"log"
"net/http"
"net/url"
"github.com/go-chi/chi/v5"
)
func index(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
db := dbFromContext(ctx)
tpl := templateFromContext(ctx)
loginToken := loginTokenFromContext(ctx)
if loginToken == nil {
http.Redirect(w, req, "/login", http.StatusFound)
return
}
me, err := db.FetchUser(ctx, loginToken.User)
if err != nil {
httpError(w, err)
return
}
clients, err := db.ListClients(ctx, loginToken.User)
if err != nil {
httpError(w, err)
return
}
data := struct {
Clients []Client
Me *User
}{
Clients: clients,
Me: me,
}
if err := tpl.ExecuteTemplate(w, "index.html", &data); err != nil {
panic(err)
}
}
func login(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
db := dbFromContext(ctx)
tpl := templateFromContext(ctx)
q := req.URL.Query()
rawRedirectURI := q.Get("redirect_uri")
if rawRedirectURI == "" {
rawRedirectURI = "/"
}
redirectURI, err := url.Parse(rawRedirectURI)
if err != nil || redirectURI.Scheme != "" || redirectURI.Opaque != "" || redirectURI.User != nil || redirectURI.Host != "" {
http.Error(w, "Invalid redirect URI", http.StatusBadRequest)
return
}
if loginTokenFromContext(ctx) != nil {
http.Redirect(w, req, redirectURI.String(), http.StatusFound)
return
}
username := req.PostFormValue("username")
password := req.PostFormValue("password")
if username == "" {
if err := tpl.ExecuteTemplate(w, "login.html", nil); err != nil {
panic(err)
}
return
}
user, err := db.FetchUserByUsername(ctx, username)
if err != nil && err != errNoDBRows {
httpError(w, fmt.Errorf("failed to fetch user: %v", err))
return
}
if err == nil {
err = user.VerifyPassword(password)
}
if err != nil {
log.Printf("login failed for user %q: %v", username, err)
// TODO: show error message
if err := tpl.ExecuteTemplate(w, "login.html", nil); err != nil {
panic(err)
}
return
}
if user.PasswordNeedsRehash() {
if err := user.SetPassword(password); err != nil {
httpError(w, fmt.Errorf("failed to rehash password: %v", err))
return
}
if err := db.StoreUser(ctx, user); err != nil {
httpError(w, fmt.Errorf("failed to store user: %v", err))
return
}
}
token := AccessToken{
User: user.ID,
Scope: internalTokenScope,
}
secret, err := token.Generate()
if err != nil {
httpError(w, fmt.Errorf("failed to generate access token: %v", err))
return
}
if err := db.CreateAccessToken(ctx, &token); err != nil {
httpError(w, fmt.Errorf("failed to create access token: %v", err))
return
}
setLoginTokenCookie(w, req, &token, secret)
http.Redirect(w, req, redirectURI.String(), http.StatusFound)
}
func logout(w http.ResponseWriter, req *http.Request) {
unsetLoginTokenCookie(w, req)
http.Redirect(w, req, "/login", http.StatusFound)
}
func updateUser(w http.ResponseWriter, req *http.Request) {
func manageUser(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
db := dbFromContext(ctx)
tpl := templateFromContext(ctx)
user := new(User)
if idStr := chi.URLParam(req, "id"); idStr != "" {
id, err := ParseID[*User](idStr)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
user, err = db.FetchUser(ctx, id)
if err != nil {
httpError(w, err)
return
}
}
loginToken := loginTokenFromContext(ctx)
if loginToken == nil {
http.Redirect(w, req, "/login", http.StatusFound)
return
}
me, err := db.FetchUser(ctx, loginToken.User)
if err != nil {
httpError(w, err)
return
} else if loginToken.User != user.ID && !me.Admin {
http.Error(w, "Access denied", http.StatusForbidden)
return
}
username := req.PostFormValue("username")
password := req.PostFormValue("password")
admin := req.PostFormValue("admin") == "on"
if username == "" {
data := struct {
User *User
Me *User
}{
User: user,
Me: me,
}
if err := tpl.ExecuteTemplate(w, "update-user.html", &data); err != nil {
if err := tpl.ExecuteTemplate(w, "manage-user.html", &data); err != nil {
panic(err)
}
return
}
user.Username = username
if me.Admin && user.ID != me.ID {
user.Admin = admin
}
if password != "" {
if err := user.SetPassword(password); err != nil {
httpError(w, err)
return
}
}
if err := db.StoreUser(ctx, user); err != nil {
httpError(w, err)
return
}
http.Redirect(w, req, "/", http.StatusFound)
}