diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go new file mode 100644 index 0000000..bc44866 --- /dev/null +++ b/internal/handlers/auth.go @@ -0,0 +1,186 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "strings" +) + +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) + + 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 strings.Contains(err.Error(), "UNIQUE") { + 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, _, 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.respondJSON(writer, request, map[string]any{ + "id": sessionID, + "nick": payload.Nick, + "token": token, + }, http.StatusOK) +}