More stuff for login
This commit is contained in:
@@ -13,6 +13,18 @@ import (
|
|||||||
//go:embed embed
|
//go:embed embed
|
||||||
var editorFS embed.FS
|
var editorFS embed.FS
|
||||||
|
|
||||||
|
const loginPage = `
|
||||||
|
<script src="https://unpkg.com/htmx-ext-json-enc@2.0.1/json-enc.js"></script>
|
||||||
|
<form id="loginForm" hx-post="https://api.darkstorm.tech/user/login" hx-ext="json-enc">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input name="username" id="usernameInput"></input>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input name="password" type="password" id="passwordInput"></input>
|
||||||
|
<p id="formResult"></p>
|
||||||
|
<button id="loginButton" type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
`
|
||||||
|
|
||||||
type Editor struct {
|
type Editor struct {
|
||||||
blogApp *blog.BlogApp
|
blogApp *blog.BlogApp
|
||||||
back *backend.Backend
|
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) {
|
func (e Editor) LoginPage(w http.ResponseWriter, r *http.Request) {
|
||||||
page, err := editorFS.Open("embed/login.html")
|
sendContent(w, r, loginPage, "", "")
|
||||||
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 {
|
func (e Editor) TrueLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Println("error reading login.html:", err)
|
//TODO
|
||||||
sendContent(w, r, "error getting page", "", "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sendContent(w, r, string(dat), "", "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Editor) Editor(w http.ResponseWriter, r *http.Request) {
|
func (e Editor) Editor(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<div id="invisiblePusher" hx-push-url="/editor"></div>
|
|
||||||
<form id="loginForm" onsubmit="login(event)">
|
|
||||||
<label for="username">Username:</label>
|
|
||||||
<input name="username" id="usernameInput"></input>
|
|
||||||
<label for="password">Password:</label>
|
|
||||||
<input name="password" type="password" id="passwordInput"></input>
|
|
||||||
<p id="formResult"></p>
|
|
||||||
<button id="loginButton" type="submit">Login</button>
|
|
||||||
</form>
|
|
||||||
@@ -25,7 +25,7 @@ type Backend struct {
|
|||||||
corsAddr string
|
corsAddr string
|
||||||
jwtPriv ed25519.PrivateKey
|
jwtPriv ed25519.PrivateKey
|
||||||
jwtPub ed25519.PublicKey
|
jwtPub ed25519.PublicKey
|
||||||
userMutex sync.RWMutex
|
userCreateMutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new Backend with the given apps. keyTable must be specified.
|
// Create a new Backend with the given apps. keyTable must be specified.
|
||||||
@@ -34,7 +34,7 @@ func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) {
|
|||||||
keyTable: keyTable,
|
keyTable: keyTable,
|
||||||
m: &http.ServeMux{},
|
m: &http.ServeMux{},
|
||||||
apps: make(map[string]App),
|
apps: make(map[string]App),
|
||||||
userMutex: sync.RWMutex{},
|
userCreateMutex: sync.Mutex{},
|
||||||
}
|
}
|
||||||
b.m.Handle("GET /robots.txt", http.FileServerFS(robotEmbed))
|
b.m.Handle("GET /robots.txt", http.FileServerFS(robotEmbed))
|
||||||
var hasLog, hasCrash bool
|
var hasLog, hasCrash bool
|
||||||
|
|||||||
+44
-49
@@ -6,9 +6,9 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@@ -56,6 +56,38 @@ type User struct {
|
|||||||
PasswordChange int64 `json:"passwordChange" bson:"passwordChange"`
|
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) {
|
func NewUser(username, password, email string) (User, error) {
|
||||||
id, err := uuid.NewV7()
|
id, err := uuid.NewV7()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -139,8 +171,8 @@ func (b *Backend) createUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO: filter offensive words/phrases
|
// TODO: filter offensive words/phrases
|
||||||
b.userMutex.Lock()
|
b.userCreateMutex.Lock()
|
||||||
defer b.userMutex.Unlock()
|
defer b.userCreateMutex.Unlock()
|
||||||
matchUsername, err := b.userTable.Find(r.Context(), map[string]any{"username": req.Username})
|
matchUsername, err := b.userTable.Find(r.Context(), map[string]any{"username": req.Username})
|
||||||
if err != nil && !errors.Is(err, ErrNotFound) {
|
if err != nil && !errors.Is(err, ErrNotFound) {
|
||||||
log.Println("error when checking for username collisions:", err)
|
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")
|
ReturnError(w, http.StatusBadRequest, "badRequest", "Bad Request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b.userMutex.Lock()
|
|
||||||
defer b.userMutex.Unlock()
|
|
||||||
err = b.userTable.Remove(r.Context(), userID)
|
err = b.userTable.Remove(r.Context(), userID)
|
||||||
if err != nil && err != ErrNotFound {
|
if err != nil && err != ErrNotFound {
|
||||||
log.Println("error deleting user:", err)
|
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")
|
ReturnError(w, http.StatusBadRequest, "invalidBody", "Bad request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b.userMutex.RLock()
|
|
||||||
defer b.userMutex.RUnlock()
|
|
||||||
var ret loginReturn
|
var ret loginReturn
|
||||||
users, err := b.userTable.Find(r.Context(), map[string]any{"username": req.Username})
|
u, err := b.TryLogin(r.Context(), req.Username, req.Password)
|
||||||
if errors.Is(err, ErrNotFound) || len(users) != 1 {
|
if err == nil {
|
||||||
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 {
|
|
||||||
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)
|
|
||||||
ReturnError(w, http.StatusInternalServerError, "internal", "Server error")
|
ReturnError(w, http.StatusInternalServerError, "internal", "Server error")
|
||||||
return
|
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 {
|
} else {
|
||||||
ret.Error = "invalid"
|
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"
|
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()
|
|
||||||
}
|
}
|
||||||
b.userTable.PartUpdate(r.Context(), u.ID, upd)
|
}
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
json.NewEncoder(w).Encode(ret)
|
json.NewEncoder(w).Encode(ret)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user