diff --git a/editor.go b/editor.go index ce1bceb..341b999 100644 --- a/editor.go +++ b/editor.go @@ -2,55 +2,76 @@ package main import ( "embed" + "fmt" "io" "log" "net/http" + "time" "github.com/CalebQ42/darkstorm-server/internal/backend" - "github.com/CalebQ42/darkstorm-server/internal/blog" ) //go:embed embed var editorFS embed.FS const loginPage = ` - -
` -type Editor struct { - 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) { +func LoginPage(w http.ResponseWriter, r *http.Request) { sendContent(w, r, loginPage, "", "") } -func (e Editor) TrueLogin(w http.ResponseWriter, r *http.Request) { - //TODO +func TrueLogin(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("HX-Request") != "true" { + sendContent(w, r, "Bad request
", "", "") + } + 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("Timed out for", time.Unix(u.Timeout, 0).Sub(time.Now()), "
"), "", "") + } else if err == backend.ErrLoginTimeout { + sendContent(w, r, "Username or password invalid
", "", "") + } else { + log.Println("error trying to login:", err) + sendContent(w, r, "Server error
", "", "") + } + return + } + tok, err := back.GenerateJWT(u.ToReqUser()) + if err != nil { + log.Println("error trying to generate JWT:", err) + sendContent(w, r, "Server error
", "", "") + 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, "Successful Login
", "", "") } -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 +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) } - w.Header().Set("Content-Type", "text/html") - http.Redirect(w, r, "https://darkstorm.tech/login", http.StatusSeeOther) + 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 } 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", "", "") return } - dat, err := io.ReadAll(page) + _, err = io.ReadAll(page) if err != nil { log.Println("error reading editor.html:", err) sendContent(w, r, "error getting page", "", "") return } - sendContent(w, r, string(dat), "", "") + sendContent(w, r, "Hello there, "+usr.Username+"
", "", "") +} + +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) } diff --git a/internal/backend/db.go b/internal/backend/db.go index ab85765..1363de0 100644 --- a/internal/backend/db.go +++ b/internal/backend/db.go @@ -14,7 +14,7 @@ type 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) Insert(ctx context.Context, data T) error Remove(ctx context.Context, ID string) error diff --git a/internal/backend/db/mongo.go b/internal/backend/db/mongo.go index dd0a8eb..834d9c2 100644 --- a/internal/backend/db/mongo.go +++ b/internal/backend/db/mongo.go @@ -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}) if res.Err() == mongo.ErrNoDocuments { - return nil, backend.ErrNotFound + return data, backend.ErrNotFound } else if res.Err() != nil { - return nil, res.Err() + return data, res.Err() } var out T err = res.Decode(&out) - return &out, err + return out, err } func (m *MongoTable[T]) Find(ctx context.Context, values map[string]any) ([]T, error) { diff --git a/internal/backend/header.go b/internal/backend/header.go index 9af1dad..f89f641 100644 --- a/internal/backend/header.go +++ b/internal/backend/header.go @@ -7,8 +7,6 @@ import ( "net/http" "strings" "time" - - "github.com/golang-jwt/jwt/v5" ) var ( @@ -64,41 +62,18 @@ func (b *Backend) ParseHeader(r *http.Request) (*ParsedHeader, error) { } out.Key = &keys[0] } - token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") - if token != "" && b.userTable != nil { - 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 out, errors.Join(ErrTokenUnauthorized, 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() + if b.userTable == nil || r.Header.Get("Authorization") == "" { + return out, nil } + token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + if token == "" { + return out, nil + } + usr, err := b.VerifyUser(r.Context(), token) + if err != nil { + return out, err + } + out.User = usr.ToReqUser() return out, nil } diff --git a/internal/backend/user.go b/internal/backend/user.go index fe7e759..ed2b03e 100644 --- a/internal/backend/user.go +++ b/internal/backend/user.go @@ -88,6 +88,41 @@ func (b *Backend) TryLogin(ctx context.Context, username, password string) (User 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) { id, err := uuid.NewV7() if err != nil { @@ -115,7 +150,7 @@ func (u User) GetID() string { return u.ID } -func (u User) toReqUser() *ReqestUser { +func (u User) ToReqUser() *ReqestUser { return &ReqestUser{ Perm: u.Perm, ID: u.ID, @@ -205,7 +240,7 @@ func (b *Backend) createUser(w http.ResponseWriter, r *http.Request) { } var ret createUserReturn ret.Username = u.Username - ret.Token, err = b.GenerateJWT(u.toReqUser()) + ret.Token, err = b.GenerateJWT(u.ToReqUser()) if err != nil { log.Println("error generating token:", err) ReturnError(w, http.StatusInternalServerError, "internal", "Server error") @@ -264,7 +299,7 @@ func (b *Backend) login(w http.ResponseWriter, r *http.Request) { var ret loginReturn u, err := b.TryLogin(r.Context(), req.Username, req.Password) if err == nil { - ret.Token, err = b.GenerateJWT(u.toReqUser()) + ret.Token, err = b.GenerateJWT(u.ToReqUser()) if err != nil { ReturnError(w, http.StatusInternalServerError, "internal", "Server error") return @@ -272,7 +307,7 @@ func (b *Backend) login(w http.ResponseWriter, r *http.Request) { } else { if err == ErrLoginTimeout { 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 } else { ret.Error = "incorrect" diff --git a/main.go b/main.go index 6147731..344ad2e 100644 --- a/main.go +++ b/main.go @@ -152,7 +152,8 @@ func setupWebsite(mux *http.ServeMux) { mux.HandleFunc("GET /portfolio", portfolioRequest) mux.HandleFunc("GET /list", blogListHandle) 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) }