diff --git a/editor.go b/editor.go index ed76e42..ce1bceb 100644 --- a/editor.go +++ b/editor.go @@ -13,6 +13,18 @@ import ( //go:embed embed var editorFS embed.FS +const loginPage = ` + +
+` + type Editor struct { blogApp *blog.BlogApp back *backend.Backend @@ -23,20 +35,11 @@ func NewBlogEditor(blogApp *blog.BlogApp, back *backend.Backend) Editor { } func (e Editor) LoginPage(w http.ResponseWriter, r *http.Request) { - page, err := editorFS.Open("embed/login.html") - defer page.Close() - if err != nil { - log.Println("error getting login.html:", err) - sendContent(w, r, "error getting page", "", "") - return - } - dat, err := io.ReadAll(page) - if err != nil { - log.Println("error reading login.html:", err) - sendContent(w, r, "error getting page", "", "") - return - } - sendContent(w, r, string(dat), "", "") + sendContent(w, r, loginPage, "", "") +} + +func (e Editor) TrueLogin(w http.ResponseWriter, r *http.Request) { + //TODO } func (e Editor) Editor(w http.ResponseWriter, r *http.Request) { diff --git a/embed/login.html b/embed/login.html deleted file mode 100644 index c6439c4..0000000 --- a/embed/login.html +++ /dev/null @@ -1,9 +0,0 @@ - - diff --git a/internal/backend/darkstorm.go b/internal/backend/darkstorm.go index c96cde1..a76dc18 100644 --- a/internal/backend/darkstorm.go +++ b/internal/backend/darkstorm.go @@ -25,16 +25,16 @@ type Backend struct { corsAddr string jwtPriv ed25519.PrivateKey jwtPub ed25519.PublicKey - userMutex sync.RWMutex + userCreateMutex sync.Mutex } // Create a new Backend with the given apps. keyTable must be specified. func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) { b := &Backend{ - keyTable: keyTable, - m: &http.ServeMux{}, - apps: make(map[string]App), - userMutex: sync.RWMutex{}, + keyTable: keyTable, + m: &http.ServeMux{}, + apps: make(map[string]App), + userCreateMutex: sync.Mutex{}, } b.m.Handle("GET /robots.txt", http.FileServerFS(robotEmbed)) var hasLog, hasCrash bool diff --git a/internal/backend/user.go b/internal/backend/user.go index f099987..fe7e759 100644 --- a/internal/backend/user.go +++ b/internal/backend/user.go @@ -6,9 +6,9 @@ import ( "encoding/base64" "encoding/json" "errors" + "fmt" "log" "net/http" - "strconv" "time" "github.com/golang-jwt/jwt/v5" @@ -56,6 +56,38 @@ type User struct { PasswordChange int64 `json:"passwordChange" bson:"passwordChange"` } +var ( + ErrLoginTimeout = errors.New("user is timed out") + ErrLoginIncorrect = errors.New("username or password is incorrect") +) + +// Tries to login with the given username and password. +// If the user exists, but is timed out, the user is still returned. +func (b *Backend) TryLogin(ctx context.Context, username, password string) (User, error) { + users, err := b.userTable.Find(ctx, map[string]any{"username": username}) + if err == ErrNotFound { + return User{}, ErrLoginIncorrect + } + if len(users) > 0 { + log.Println("duplicate username detected, fix immediately:", username) + } + user := users[0] + if time.Unix(user.Timeout, 0).After(time.Now()) { + return user, ErrLoginTimeout + } + if valid, _ := user.ValidatePassword(password); !valid { + upd := map[string]any{"fails": user.Fails + 1} + if (user.Fails+1)%3 == 0 { + minutes := 3 ^ (((user.Fails + 1) / 3) - 1) + upd["timeout"] = time.Now().Add(time.Minute * time.Duration(minutes)).Unix() + b.userTable.PartUpdate(ctx, user.ID, upd) + return user, ErrLoginTimeout + } + return User{}, ErrLoginIncorrect + } + return user, nil +} + func NewUser(username, password, email string) (User, error) { id, err := uuid.NewV7() if err != nil { @@ -139,8 +171,8 @@ func (b *Backend) createUser(w http.ResponseWriter, r *http.Request) { return } // TODO: filter offensive words/phrases - b.userMutex.Lock() - defer b.userMutex.Unlock() + b.userCreateMutex.Lock() + defer b.userCreateMutex.Unlock() matchUsername, err := b.userTable.Find(r.Context(), map[string]any{"username": req.Username}) if err != nil && !errors.Is(err, ErrNotFound) { log.Println("error when checking for username collisions:", err) @@ -196,8 +228,6 @@ func (b *Backend) deleteUser(w http.ResponseWriter, r *http.Request) { ReturnError(w, http.StatusBadRequest, "badRequest", "Bad Request") return } - b.userMutex.Lock() - defer b.userMutex.Unlock() err = b.userTable.Remove(r.Context(), userID) if err != nil && err != ErrNotFound { log.Println("error deleting user:", err) @@ -231,58 +261,23 @@ func (b *Backend) login(w http.ResponseWriter, r *http.Request) { ReturnError(w, http.StatusBadRequest, "invalidBody", "Bad request") return } - b.userMutex.RLock() - defer b.userMutex.RUnlock() var ret loginReturn - users, err := b.userTable.Find(r.Context(), map[string]any{"username": req.Username}) - if errors.Is(err, ErrNotFound) || len(users) != 1 { - ret.Error = "invalid" - ret.ErrorMsg = "Incorrect username or password" - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(ret) - return - } - u := users[0] - if time.Unix(u.Timeout, 0).After(time.Now()) { - ret.Error = "timeout" - ret.Timeout = u.Timeout - time.Now().Unix() - ret.ErrorMsg = "Timed out for " + strconv.Itoa(int(ret.Timeout)) + " seconds" - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(ret) - return - } - hash, err := u.HashPassword(req.Password) - if err != nil { - log.Println("error hashing request password:", err) - ReturnError(w, http.StatusInternalServerError, "internal", "Server error") - return - } - if u.Password == hash { + u, err := b.TryLogin(r.Context(), req.Username, req.Password) + if err == nil { ret.Token, err = b.GenerateJWT(u.toReqUser()) if err != nil { - log.Println("error generating token:", err) ReturnError(w, http.StatusInternalServerError, "internal", "Server error") return } - json.NewEncoder(w).Encode(ret) - if u.Fails != 0 { - err = b.userTable.PartUpdate(context.Background(), u.ID, map[string]any{"fails": 0}) - if err != nil { - log.Println("error resetting fails after successful login:", err) - } - } } else { - ret.Error = "invalid" - ret.ErrorMsg = "Incorrect username or password" - upd := map[string]any{"fails": u.Fails + 1} - if (u.Fails+1)%3 == 0 { - minutes := 3 ^ ((u.Fails / 3) - 1) - timeout := time.Now().Add(time.Duration(minutes) * time.Minute).Unix() - upd["timeout"] = timeout - ret.Timeout = timeout - time.Now().Unix() + if err == ErrLoginTimeout { + ret.Error = "timeout" + ret.ErrorMsg = fmt.Sprint("Timed out for", u.Timeout, "seconds") + ret.Timeout = u.Timeout + } else { + ret.Error = "incorrect" + ret.ErrorMsg = "Incorrect username or password" } - b.userTable.PartUpdate(r.Context(), u.ID, upd) - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(ret) } + json.NewEncoder(w).Encode(ret) }