This commit is contained in:
vipg
2026-02-09 16:49:18 +08:00
parent 6640f09639
commit bdb1065217
6 changed files with 261 additions and 141 deletions

View File

@@ -0,0 +1,86 @@
package handler
import (
"encoding/json"
"net/http"
"common/types"
"user/internal/model"
"user/internal/service"
)
type Handler struct {
S *service.Service
}
func New(s *service.Service) *Handler {
return &Handler{S: s}
}
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, false, "method not allowed", nil)
return
}
var req model.RegisterReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, false, "invalid json", nil)
return
}
userID, err := h.S.Register(req.Account, req.Password)
if err != nil {
switch err {
case service.ErrInvalidInput:
writeJSON(w, http.StatusBadRequest, false, "invalid account or password", nil)
case service.ErrAccountExists:
writeJSON(w, http.StatusConflict, false, "account exists", nil)
default:
writeJSON(w, http.StatusInternalServerError, false, "internal error", nil)
}
return
}
writeJSON(w, http.StatusCreated, true, "ok", map[string]string{"user_id": userID})
}
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, false, "method not allowed", nil)
return
}
var req model.LoginReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, false, "invalid json", nil)
return
}
userID, err := h.S.Login(req.Account, req.Password)
if err != nil {
switch err {
case service.ErrInvalidInput:
writeJSON(w, http.StatusBadRequest, false, "invalid account or password", nil)
case service.ErrUnauthorized:
writeJSON(w, http.StatusUnauthorized, false, "unauthorized", nil)
default:
writeJSON(w, http.StatusInternalServerError, false, "internal error", nil)
}
return
}
writeJSON(w, http.StatusOK, true, "ok", map[string]string{"user_id": userID})
}
func (h *Handler) Healthz(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, true, "ok", nil)
}
func (h *Handler) Version(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, true, "ok", map[string]string{"version": "user-service v0.1.0"})
}
func (h *Handler) Root(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, true, "ok", map[string]string{"service": "user"})
}
func writeJSON(w http.ResponseWriter, code int, status bool, msg string, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(types.Response{Status: status, Message: msg, Data: data})
}

View File

@@ -0,0 +1,11 @@
package model
type RegisterReq struct {
Account string `json:"account"`
Password string `json:"password"`
}
type LoginReq struct {
Account string `json:"account"`
Password string `json:"password"`
}

View File

@@ -0,0 +1,47 @@
package repository
import (
"database/sql"
)
type Repo struct {
DB *sql.DB
}
func New(db *sql.DB) *Repo {
return &Repo{DB: db}
}
func (r *Repo) CreateUser(tx *sql.Tx) (string, error) {
var userID string
if err := tx.QueryRow(`INSERT INTO users DEFAULT VALUES RETURNING user_id`).Scan(&userID); err != nil {
return "", err
}
return userID, nil
}
func (r *Repo) CreateLoginAccount(tx *sql.Tx, userID, account string) error {
_, err := tx.Exec(`INSERT INTO user_login_accounts (user_id, value, deleted) VALUES ($1, $2, false)`, userID, account)
return err
}
func (r *Repo) CreateLoginPassword(tx *sql.Tx, userID, hashed string) error {
_, err := tx.Exec(`INSERT INTO user_login_passwords (user_id, value, deleted) VALUES ($1, $2, false)`, userID, hashed)
return err
}
func (r *Repo) GetUserIDByAccount(account string) (string, error) {
var userID string
if err := r.DB.QueryRow(`SELECT user_id FROM user_login_accounts WHERE value = $1 AND deleted = false`, account).Scan(&userID); err != nil {
return "", err
}
return userID, nil
}
func (r *Repo) GetHashedPassword(userID string) (string, error) {
var hashed string
if err := r.DB.QueryRow(`SELECT value FROM user_login_passwords WHERE user_id = $1 AND deleted = false`, userID).Scan(&hashed); err != nil {
return "", err
}
return hashed, nil
}

View File

@@ -0,0 +1,17 @@
package router
import (
"net/http"
"user/internal/handler"
)
func New(h *handler.Handler) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", h.Healthz)
mux.HandleFunc("/version", h.Version)
mux.HandleFunc("/", h.Root)
mux.HandleFunc("/register", h.Register)
mux.HandleFunc("/login", h.Login)
return mux
}

View File

@@ -0,0 +1,92 @@
package service
import (
"database/sql"
"errors"
"github.com/jackc/pgconn"
"golang.org/x/crypto/bcrypt"
"user/internal/repository"
)
var (
ErrInvalidInput = errors.New("invalid_input")
ErrAccountExists = errors.New("account_exists")
ErrUnauthorized = errors.New("unauthorized")
)
type Service struct {
Repo *repository.Repo
}
func New(repo *repository.Repo) *Service {
return &Service{Repo: repo}
}
func (s *Service) Register(account, password string) (string, error) {
if !validAccount(account) || !validPassword(password) {
return "", ErrInvalidInput
}
hashed, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return "", err
}
tx, err := s.Repo.DB.Begin()
if err != nil {
return "", err
}
defer func() { _ = tx.Rollback() }()
userID, err := s.Repo.CreateUser(tx)
if err != nil {
return "", err
}
if err := s.Repo.CreateLoginAccount(tx, userID, account); err != nil {
if isUniqueViolation(err) {
return "", ErrAccountExists
}
return "", err
}
if err := s.Repo.CreateLoginPassword(tx, userID, string(hashed)); err != nil {
return "", err
}
if err := tx.Commit(); err != nil {
return "", err
}
return userID, nil
}
func (s *Service) Login(account, password string) (string, error) {
if !validAccount(account) || !validPassword(password) {
return "", ErrInvalidInput
}
userID, err := s.Repo.GetUserIDByAccount(account)
if err != nil {
return "", ErrUnauthorized
}
hashed, err := s.Repo.GetHashedPassword(userID)
if err != nil {
return "", ErrUnauthorized
}
if bcrypt.CompareHashAndPassword([]byte(hashed), []byte(password)) != nil {
return "", ErrUnauthorized
}
return userID, nil
}
func validAccount(a string) bool {
n := len(a)
return n >= 3 && n <= 100
}
func validPassword(p string) bool {
n := len(p)
return n >= 8 && n <= 128
}
func isUniqueViolation(err error) bool {
var pe *pgconn.PgError
if errors.As(err, &pe) {
return pe.Code == "23505"
}
return false
}