Add API signin URL

That was we can have much more specific failure modes for API clients
This commit is contained in:
Eli Ribble 2026-01-25 19:36:56 +00:00
parent c0b6398de2
commit 82081b9609
No known key found for this signature in database
3 changed files with 62 additions and 1 deletions

View file

@ -22,6 +22,7 @@ func AddRoutes(r chi.Router) {
// Unauthenticated endpoints
r.Get("/district", apiGetDistrict)
r.Get("/district/{slug}/logo", apiGetDistrictLogo)
r.Post("/signin", postSignin)
r.Post("/twilio/message", twilioMessagePost)
r.Post("/twilio/status", twilioStatusPost)
r.Post("/twilio/text", twilioTextPost)

45
api/signin.go Normal file
View file

@ -0,0 +1,45 @@
package api
import (
"errors"
"fmt"
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/auth"
"github.com/go-chi/render"
"github.com/rs/zerolog/log"
)
func postSignin(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
render.Render(w, r, errRender(fmt.Errorf("Failed to parse POST form: %w", err)))
return
}
username := r.FormValue("username")
password := r.FormValue("password")
if password == "" || username == "" {
w.Header().Set("WWW-Authenticate-Error", "no-credentials")
http.Error(w, "invalid-credentials", http.StatusUnauthorized)
return
}
log.Info().Str("username", username).Msg("API Signin")
_, err := auth.SigninUser(r, username, password)
if err != nil {
if errors.Is(err, auth.InvalidCredentials{}) {
w.Header().Set("WWW-Authenticate-Error", "invalid-credentials")
http.Error(w, "invalid-credentials", http.StatusUnauthorized)
return
}
if errors.Is(err, auth.InvalidUsername{}) {
w.Header().Set("WWW-Authenticate-Error", "invalid-credentials")
http.Error(w, "invalid-credentials", http.StatusUnauthorized)
return
}
http.Error(w, "signin-server-error", http.StatusInternalServerError)
return
}
http.Error(w, "", http.StatusAccepted)
}

View file

@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
@ -188,6 +189,18 @@ func hashPassword(password string) (string, error) {
return string(bytes), err
}
func redact(s string) string {
if len(s) <= 4 {
return s
}
first_two := s[:2]
last_two := s[len(s)-2:]
middle_length := len(s) - 4
return first_two + strings.Repeat("*", middle_length) + last_two
}
func validatePassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
@ -198,17 +211,18 @@ func validateUser(ctx context.Context, username string, password string) (*model
if err != nil {
return nil, fmt.Errorf("Failed to hash password: %w", err)
}
log.Info().Str("username", username).Str("password", password).Str("hash", passwordHash).Msg("Validating user")
result, err := sql.UserByUsername(username).All(ctx, db.PGInstance.BobDB)
if err != nil {
return nil, fmt.Errorf("Failed to query for user: %w", err)
}
switch len(result) {
case 0:
log.Info().Str("username", username).Str("password", redact(password)).Msg("Invalid username")
return nil, InvalidUsername{}
case 1:
row := result[0]
if !validatePassword(password, row.PasswordHash) {
log.Info().Str("username", username).Str("password", redact(password)).Str("hash", passwordHash).Msg("Invalid password for user")
return nil, InvalidCredentials{}
}
user := models.User{
@ -223,6 +237,7 @@ func validateUser(ctx context.Context, username string, password string) (*model
OrganizationID: row.OrganizationID,
Username: row.Username,
}
log.Info().Str("username", username).Msg("Validated user")
return &user, nil
default:
return nil, errors.New("More than one matching row, this should be impossible.")