Do more things through main web stuff instead of API

This commit is contained in:
Caleb Gardner
2024-11-11 08:00:53 -06:00
parent f04c00c015
commit e183eefee8
6 changed files with 113 additions and 73 deletions
+56 -27
View File
@@ -2,55 +2,76 @@ package main
import ( import (
"embed" "embed"
"fmt"
"io" "io"
"log" "log"
"net/http" "net/http"
"time"
"github.com/CalebQ42/darkstorm-server/internal/backend" "github.com/CalebQ42/darkstorm-server/internal/backend"
"github.com/CalebQ42/darkstorm-server/internal/blog"
) )
//go:embed embed //go:embed embed
var editorFS embed.FS var editorFS embed.FS
const loginPage = ` const loginPage = `
<script src="https://unpkg.com/htmx-ext-json-enc@2.0.1/json-enc.js"></script> <form id="loginForm" hx-post="/login">
<form id="loginForm" hx-post="https://api.darkstorm.tech/user/login" hx-ext="json-enc">
<label for="username">Username:</label> <label for="username">Username:</label>
<input name="username" id="usernameInput"></input> <input name="username" id="usernameInput" onkeydown="return event.key != 'Enter';"></input>
<label for="password">Password:</label> <label for="password">Password:</label>
<input name="password" type="password" id="passwordInput"></input> <input name="password" type="password" id="passwordInput"></input>
<p id="formResult"></p> <div id="formResult"></div>
<button id="loginButton" type="submit">Login</button> <button id="loginButton" type="submit">Login</button>
</form> </form>
` `
type Editor struct { func LoginPage(w http.ResponseWriter, r *http.Request) {
blogApp *blog.BlogApp
back *backend.Backend
}
func NewBlogEditor(blogApp *blog.BlogApp, back *backend.Backend) Editor {
return Editor{blogApp: blogApp, back: back}
}
func (e Editor) LoginPage(w http.ResponseWriter, r *http.Request) {
sendContent(w, r, loginPage, "", "") sendContent(w, r, loginPage, "", "")
} }
func (e Editor) TrueLogin(w http.ResponseWriter, r *http.Request) { func TrueLogin(w http.ResponseWriter, r *http.Request) {
//TODO if r.Header.Get("HX-Request") != "true" {
sendContent(w, r, "<p>Bad request</p>", "", "")
}
u, err := back.TryLogin(r.Context(), r.URL.Query().Get("username"), r.URL.Query().Get("password"))
if err != nil {
if err == backend.ErrLoginTimeout {
sendContent(w, r, fmt.Sprint("<p>Timed out for", time.Unix(u.Timeout, 0).Sub(time.Now()), "</p>"), "", "")
} else if err == backend.ErrLoginTimeout {
sendContent(w, r, "<p>Username or password invalid</p>", "", "")
} else {
log.Println("error trying to login:", err)
sendContent(w, r, "<p>Server error</p>", "", "")
} }
func (e Editor) Editor(w http.ResponseWriter, r *http.Request) {
hdr, err := back.ParseHeader(r)
if err == backend.ErrApiKeyUnauthorized || err == backend.ErrTokenUnauthorized || hdr == nil || hdr.User == nil {
if r.Header.Get("HX-Request") == "true" {
w.Header().Set("HX-Location", `{"path":"/login", "target":"#content"}`)
return return
} }
w.Header().Set("Content-Type", "text/html") tok, err := back.GenerateJWT(u.ToReqUser())
http.Redirect(w, r, "https://darkstorm.tech/login", http.StatusSeeOther) if err != nil {
log.Println("error trying to generate JWT:", err)
sendContent(w, r, "<p>Server error</p>", "", "")
return
}
w.Header().Set("Set-Cookie", "blogAuthToken="+tok+"; Secure; Max-Age=43170") // Max-Age is 11.5 hours. JWTs are valid for 12 hours.
sendContent(w, r, "<p hx-get='/editor' hx-push-url='true' hx-trigger='load' hx-target='#content'>Successful Login</p>", "", "")
}
func Editor(w http.ResponseWriter, r *http.Request) {
authCookie, err := r.Cookie("blogAuthToken")
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
if err != http.ErrNoCookie {
log.Println("error getting auth cookie:", err)
}
editorRedirect(w, r, "/login")
return
}
usr, err := back.VerifyUser(r.Context(), authCookie.Value)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
if err != backend.ErrTokenUnauthorized {
log.Println("error authorizing JWT token:", err)
}
editorRedirect(w, r, "/login")
return return
} }
page, err := editorFS.Open("embed/editor.html") page, err := editorFS.Open("embed/editor.html")
@@ -60,11 +81,19 @@ func (e Editor) Editor(w http.ResponseWriter, r *http.Request) {
sendContent(w, r, "error getting page", "", "") sendContent(w, r, "error getting page", "", "")
return return
} }
dat, err := io.ReadAll(page) _, err = io.ReadAll(page)
if err != nil { if err != nil {
log.Println("error reading editor.html:", err) log.Println("error reading editor.html:", err)
sendContent(w, r, "error getting page", "", "") sendContent(w, r, "error getting page", "", "")
return return
} }
sendContent(w, r, string(dat), "", "") sendContent(w, r, "<p>Hello there, "+usr.Username+"</p>", "", "")
}
func editorRedirect(w http.ResponseWriter, r *http.Request, path string) {
if r.Header.Get("HX-Request") == "true" {
w.Header().Set("HX-Location", `{"path": "`+path+`", "target":"#content"}`)
return
}
http.Redirect(w, r, "https://darkstorm.tech"+path, http.StatusSeeOther)
} }
+1 -1
View File
@@ -14,7 +14,7 @@ type IDStruct interface {
} }
type Table[T IDStruct] interface { type Table[T IDStruct] interface {
Get(ctx context.Context, ID string) (data *T, err error) Get(ctx context.Context, ID string) (data T, err error)
Find(ctx context.Context, values map[string]any) ([]T, error) Find(ctx context.Context, values map[string]any) ([]T, error)
Insert(ctx context.Context, data T) error Insert(ctx context.Context, data T) error
Remove(ctx context.Context, ID string) error Remove(ctx context.Context, ID string) error
+4 -4
View File
@@ -18,16 +18,16 @@ func NewMongoTable[T backend.IDStruct](col *mongo.Collection) *MongoTable[T] {
} }
} }
func (m *MongoTable[T]) Get(ctx context.Context, ID string) (data *T, err error) { func (m *MongoTable[T]) Get(ctx context.Context, ID string) (data T, err error) {
res := m.col.FindOne(ctx, bson.M{"_id": ID}) res := m.col.FindOne(ctx, bson.M{"_id": ID})
if res.Err() == mongo.ErrNoDocuments { if res.Err() == mongo.ErrNoDocuments {
return nil, backend.ErrNotFound return data, backend.ErrNotFound
} else if res.Err() != nil { } else if res.Err() != nil {
return nil, res.Err() return data, res.Err()
} }
var out T var out T
err = res.Decode(&out) err = res.Decode(&out)
return &out, err return out, err
} }
func (m *MongoTable[T]) Find(ctx context.Context, values map[string]any) ([]T, error) { func (m *MongoTable[T]) Find(ctx context.Context, values map[string]any) ([]T, error) {
+9 -34
View File
@@ -7,8 +7,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/golang-jwt/jwt/v5"
) )
var ( var (
@@ -64,41 +62,18 @@ func (b *Backend) ParseHeader(r *http.Request) (*ParsedHeader, error) {
} }
out.Key = &keys[0] out.Key = &keys[0]
} }
if b.userTable == nil || r.Header.Get("Authorization") == "" {
return out, nil
}
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if token != "" && b.userTable != nil { if token == "" {
t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { return out, nil
return b.jwtPub, nil }
}, jwt.WithIssuer("darkstorm.tech"), jwt.WithExpirationRequired(), jwt.WithValidMethods([]string{"EdDSA"})) usr, err := b.VerifyUser(r.Context(), token)
if err != nil { if err != nil {
return out, errors.Join(ErrTokenUnauthorized, err) return out, err
}
exp, _ := t.Claims.GetExpirationTime()
if exp.Time.Before(time.Now()) {
return out, ErrTokenUnauthorized
}
sub, err := t.Claims.GetSubject()
if err == jwt.ErrInvalidKey {
return out, ErrTokenUnauthorized
} else if err != nil {
return out, errors.Join(ErrTokenUnauthorized, err)
}
usr, err := b.userTable.Get(r.Context(), sub)
if err == jwt.ErrInvalidKey {
return out, ErrTokenUnauthorized
} else if err != nil {
return out, errors.Join(ErrTokenUnauthorized, err)
}
iss, err := t.Claims.GetIssuedAt()
if err == jwt.ErrInvalidKey {
return out, ErrTokenUnauthorized
} else if err != nil {
return out, errors.Join(ErrTokenUnauthorized, err)
}
if usr.PasswordChange > 0 && iss.Time.Before(time.Unix(usr.PasswordChange, 0)) {
return out, ErrTokenUnauthorized
}
out.User = usr.toReqUser()
} }
out.User = usr.ToReqUser()
return out, nil return out, nil
} }
+39 -4
View File
@@ -88,6 +88,41 @@ func (b *Backend) TryLogin(ctx context.Context, username, password string) (User
return user, nil return user, nil
} }
func (b *Backend) VerifyUser(ctx context.Context, token string) (*User, error) {
t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
return b.jwtPub, nil
}, jwt.WithIssuer("darkstorm.tech"), jwt.WithExpirationRequired(), jwt.WithValidMethods([]string{"EdDSA"}))
if err != nil {
return nil, err
}
exp, _ := t.Claims.GetExpirationTime()
if exp.Time.Before(time.Now()) {
return nil, ErrTokenUnauthorized
}
sub, err := t.Claims.GetSubject()
if err == jwt.ErrInvalidKey {
return nil, ErrTokenUnauthorized
} else if err != nil {
return nil, err
}
usr, err := b.userTable.Get(ctx, sub)
if err == jwt.ErrInvalidKey {
return nil, ErrTokenUnauthorized
} else if err != nil {
return nil, err
}
iss, err := t.Claims.GetIssuedAt()
if err == jwt.ErrInvalidKey {
return nil, ErrTokenUnauthorized
} else if err != nil {
return nil, errors.Join(ErrTokenUnauthorized, err)
}
if usr.PasswordChange > 0 && iss.Time.Before(time.Unix(usr.PasswordChange, 0)) {
return nil, ErrTokenUnauthorized
}
return usr, nil
}
func NewUser(username, password, email string) (User, error) { func NewUser(username, password, email string) (User, error) {
id, err := uuid.NewV7() id, err := uuid.NewV7()
if err != nil { if err != nil {
@@ -115,7 +150,7 @@ func (u User) GetID() string {
return u.ID return u.ID
} }
func (u User) toReqUser() *ReqestUser { func (u User) ToReqUser() *ReqestUser {
return &ReqestUser{ return &ReqestUser{
Perm: u.Perm, Perm: u.Perm,
ID: u.ID, ID: u.ID,
@@ -205,7 +240,7 @@ func (b *Backend) createUser(w http.ResponseWriter, r *http.Request) {
} }
var ret createUserReturn var ret createUserReturn
ret.Username = u.Username ret.Username = u.Username
ret.Token, err = b.GenerateJWT(u.toReqUser()) ret.Token, err = b.GenerateJWT(u.ToReqUser())
if err != nil { if err != nil {
log.Println("error generating token:", err) log.Println("error generating token:", err)
ReturnError(w, http.StatusInternalServerError, "internal", "Server error") ReturnError(w, http.StatusInternalServerError, "internal", "Server error")
@@ -264,7 +299,7 @@ func (b *Backend) login(w http.ResponseWriter, r *http.Request) {
var ret loginReturn var ret loginReturn
u, err := b.TryLogin(r.Context(), req.Username, req.Password) u, err := b.TryLogin(r.Context(), req.Username, req.Password)
if err == nil { if err == nil {
ret.Token, err = b.GenerateJWT(u.toReqUser()) ret.Token, err = b.GenerateJWT(u.ToReqUser())
if err != nil { if err != nil {
ReturnError(w, http.StatusInternalServerError, "internal", "Server error") ReturnError(w, http.StatusInternalServerError, "internal", "Server error")
return return
@@ -272,7 +307,7 @@ func (b *Backend) login(w http.ResponseWriter, r *http.Request) {
} else { } else {
if err == ErrLoginTimeout { if err == ErrLoginTimeout {
ret.Error = "timeout" ret.Error = "timeout"
ret.ErrorMsg = fmt.Sprint("Timed out for", u.Timeout, "seconds") ret.ErrorMsg = fmt.Sprint("Timed out for", time.Unix(u.Timeout, 0).Sub(time.Now()), "seconds")
ret.Timeout = u.Timeout ret.Timeout = u.Timeout
} else { } else {
ret.Error = "incorrect" ret.Error = "incorrect"
+2 -1
View File
@@ -152,7 +152,8 @@ func setupWebsite(mux *http.ServeMux) {
mux.HandleFunc("GET /portfolio", portfolioRequest) mux.HandleFunc("GET /portfolio", portfolioRequest)
mux.HandleFunc("GET /list", blogListHandle) mux.HandleFunc("GET /list", blogListHandle)
mux.HandleFunc("GET /login", edit.LoginPage) mux.HandleFunc("GET /login", edit.LoginPage)
mux.HandleFunc("GET /editor/", edit.Editor) mux.HandleFunc("GET /editor", edit.Editor)
mux.HandleFunc("POST /login", edit.TrueLogin)
mux.HandleFunc("/", mainHandle) mux.HandleFunc("/", mainHandle)
} }