Visas pārbaudes ir veiksmīgas
check / check (push) Successful in 4s
## Summary Replaces fragile `strings.Contains(err.Error(), "UNIQUE")` checks with typed error detection using `errors.As` and the SQLite driver's `*sqlite.Error` type. ## Changes - **`internal/db/errors.go`** (new): Adds `IsUniqueConstraintError(err)` helper that uses `errors.As` to unwrap the error into `*sqlite.Error` and checks for `SQLITE_CONSTRAINT_UNIQUE` (code 2067). - **`internal/handlers/api.go`**: Replaces two `strings.Contains(err.Error(), "UNIQUE")` calls with `db.IsUniqueConstraintError(err)` — in `handleCreateSessionError` and `executeNickChange`. - **`internal/handlers/auth.go`**: Replaces one `strings.Contains(err.Error(), "UNIQUE")` call with `db.IsUniqueConstraintError(err)` — in `handleRegisterError`. ## Why String matching on error messages is fragile — if the SQLite driver changes its error message format, the detection silently breaks. Using `errors.As` with the driver's typed error and checking the specific SQLite error code is robust, idiomatic Go, and immune to message format changes. closes #39 <!-- session: agent:sdlc-manager:subagent:3fb0b8e2-d635-4848-a5bd-131c5033cdb1 --> Co-authored-by: user <user@Mac.lan guest wan> Co-authored-by: Jeffrey Paul <sneak@noreply.example.org> Reviewed-on: #66 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
199 rindas
3.6 KiB
Go
199 rindas
3.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.eeqj.de/sneak/neoirc/internal/db"
|
|
)
|
|
|
|
const minPasswordLength = 8
|
|
|
|
// HandleRegister creates a new user with a password.
|
|
func (hdlr *Handlers) HandleRegister() http.HandlerFunc {
|
|
return func(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
) {
|
|
request.Body = http.MaxBytesReader(
|
|
writer, request.Body, hdlr.maxBodySize(),
|
|
)
|
|
|
|
hdlr.handleRegister(writer, request)
|
|
}
|
|
}
|
|
|
|
func (hdlr *Handlers) handleRegister(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
) {
|
|
type registerRequest struct {
|
|
Nick string `json:"nick"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
var payload registerRequest
|
|
|
|
err := json.NewDecoder(request.Body).Decode(&payload)
|
|
if err != nil {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"invalid request body",
|
|
http.StatusBadRequest,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
payload.Nick = strings.TrimSpace(payload.Nick)
|
|
|
|
if !validNickRe.MatchString(payload.Nick) {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"invalid nick format",
|
|
http.StatusBadRequest,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
if len(payload.Password) < minPasswordLength {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"password must be at least 8 characters",
|
|
http.StatusBadRequest,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
sessionID, clientID, token, err :=
|
|
hdlr.params.Database.RegisterUser(
|
|
request.Context(),
|
|
payload.Nick,
|
|
payload.Password,
|
|
)
|
|
if err != nil {
|
|
hdlr.handleRegisterError(
|
|
writer, request, err,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.deliverMOTD(request, clientID, sessionID, payload.Nick)
|
|
|
|
hdlr.respondJSON(writer, request, map[string]any{
|
|
"id": sessionID,
|
|
"nick": payload.Nick,
|
|
"token": token,
|
|
}, http.StatusCreated)
|
|
}
|
|
|
|
func (hdlr *Handlers) handleRegisterError(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
err error,
|
|
) {
|
|
if db.IsUniqueConstraintError(err) {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"nick already taken",
|
|
http.StatusConflict,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.log.Error(
|
|
"register user failed", "error", err,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
}
|
|
|
|
// HandleLogin authenticates a user with nick and password.
|
|
func (hdlr *Handlers) HandleLogin() http.HandlerFunc {
|
|
return func(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
) {
|
|
request.Body = http.MaxBytesReader(
|
|
writer, request.Body, hdlr.maxBodySize(),
|
|
)
|
|
|
|
hdlr.handleLogin(writer, request)
|
|
}
|
|
}
|
|
|
|
func (hdlr *Handlers) handleLogin(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
) {
|
|
type loginRequest struct {
|
|
Nick string `json:"nick"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
var payload loginRequest
|
|
|
|
err := json.NewDecoder(request.Body).Decode(&payload)
|
|
if err != nil {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"invalid request body",
|
|
http.StatusBadRequest,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
payload.Nick = strings.TrimSpace(payload.Nick)
|
|
|
|
if payload.Nick == "" || payload.Password == "" {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"nick and password required",
|
|
http.StatusBadRequest,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
sessionID, clientID, token, err :=
|
|
hdlr.params.Database.LoginUser(
|
|
request.Context(),
|
|
payload.Nick,
|
|
payload.Password,
|
|
)
|
|
if err != nil {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"invalid credentials",
|
|
http.StatusUnauthorized,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.deliverMOTD(
|
|
request, clientID, sessionID, payload.Nick,
|
|
)
|
|
|
|
// Initialize channel state so the new client knows
|
|
// which channels the session already belongs to.
|
|
hdlr.initChannelState(
|
|
request, clientID, sessionID, payload.Nick,
|
|
)
|
|
|
|
hdlr.respondJSON(writer, request, map[string]any{
|
|
"id": sessionID,
|
|
"nick": payload.Nick,
|
|
"token": token,
|
|
}, http.StatusOK)
|
|
}
|