161 lines
4.2 KiB
Go
161 lines
4.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"regexp"
|
|
"time"
|
|
|
|
"hakemsho/internal/httpx"
|
|
"hakemsho/internal/user"
|
|
)
|
|
|
|
// Handler endpointهای احراز هویت را فراهم میکند.
|
|
type Handler struct {
|
|
users *user.Repo
|
|
otp *OTPStore
|
|
kave *Kavenegar
|
|
jwt *JWT
|
|
throttle *httpx.Throttle
|
|
|
|
adminMobile string
|
|
adminOTP string
|
|
}
|
|
|
|
func NewHandler(users *user.Repo, otp *OTPStore, kave *Kavenegar, jwt *JWT, adminMobile, adminOTP string) *Handler {
|
|
return &Handler{
|
|
users: users,
|
|
otp: otp,
|
|
kave: kave,
|
|
jwt: jwt,
|
|
throttle: httpx.NewThrottle(3, time.Minute), // معادل throttle:3,1
|
|
adminMobile: adminMobile,
|
|
adminOTP: adminOTP,
|
|
}
|
|
}
|
|
|
|
// mobileRe اعتبارسنجی ساده شماره موبایل ایران.
|
|
var mobileRe = regexp.MustCompile(`^09\d{9}$`)
|
|
|
|
type loginOTPReq struct {
|
|
Mobile string `json:"mobile"`
|
|
FCMToken string `json:"fcm_token"`
|
|
}
|
|
|
|
// LoginOTP — POST /api/auth/login-otp
|
|
func (h *Handler) LoginOTP(w http.ResponseWriter, r *http.Request) {
|
|
var req loginOTPReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
if !mobileRe.MatchString(req.Mobile) {
|
|
httpx.Error(w, http.StatusUnprocessableEntity, "invalid mobile")
|
|
return
|
|
}
|
|
// throttle بر اساس موبایل: حداکثر ۳ بار در دقیقه
|
|
if !h.throttle.Allow(req.Mobile) {
|
|
httpx.Error(w, http.StatusTooManyRequests, "too many requests")
|
|
return
|
|
}
|
|
|
|
ctx := r.Context()
|
|
u, err := h.users.FindOrCreate(ctx, req.Mobile)
|
|
if err != nil {
|
|
slog.Error("find or create user", "err", err)
|
|
httpx.Error(w, http.StatusInternalServerError, "server error")
|
|
return
|
|
}
|
|
|
|
// شماره ادمین کد ثابت دارد و SMS نمیگیرد
|
|
if req.Mobile != h.adminMobile {
|
|
code := Generate()
|
|
if err := h.otp.Create(ctx, u.ID, code); err != nil {
|
|
slog.Error("create otp", "err", err)
|
|
httpx.Error(w, http.StatusInternalServerError, "server error")
|
|
return
|
|
}
|
|
// ارسال SMS بهصورت fire-and-forget (بدون صف جانبی، مطابق پلن مینیموم)
|
|
go func(mobile, code string) {
|
|
bg, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := h.kave.SendVerify(bg, mobile, code); err != nil {
|
|
slog.Error("kavenegar send", "mobile", mobile, "err", err)
|
|
}
|
|
}(req.Mobile, code)
|
|
}
|
|
|
|
httpx.JSON(w, http.StatusOK, map[string]string{"message": "otp sent"})
|
|
}
|
|
|
|
type checkOTPReq struct {
|
|
Mobile string `json:"mobile"`
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
// CheckOTP — POST /api/auth/check-otp
|
|
func (h *Handler) CheckOTP(w http.ResponseWriter, r *http.Request) {
|
|
var req checkOTPReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
httpx.Error(w, http.StatusBadRequest, "invalid body")
|
|
return
|
|
}
|
|
if req.Mobile == "" || req.Token == "" {
|
|
httpx.Error(w, http.StatusUnprocessableEntity, "mobile and token required")
|
|
return
|
|
}
|
|
|
|
ctx := r.Context()
|
|
u, err := h.users.FindByMobile(ctx, req.Mobile)
|
|
if errors.Is(err, user.ErrNotFound) {
|
|
httpx.Error(w, http.StatusNotFound, "user not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
httpx.Error(w, http.StatusInternalServerError, "server error")
|
|
return
|
|
}
|
|
|
|
// راه ادمین برای تست بدون SMS
|
|
adminBypass := h.adminMobile != "" && req.Mobile == h.adminMobile && req.Token == h.adminOTP
|
|
if !adminBypass {
|
|
if err := h.otp.Verify(ctx, u.ID, req.Token); err != nil {
|
|
httpx.Error(w, http.StatusBadRequest, "token not valid")
|
|
return
|
|
}
|
|
}
|
|
|
|
ability := "user"
|
|
if u.IsAdmin {
|
|
ability = "admin"
|
|
}
|
|
token, err := h.jwt.Issue(u.ID, ability)
|
|
if err != nil {
|
|
httpx.Error(w, http.StatusInternalServerError, "server error")
|
|
return
|
|
}
|
|
|
|
httpx.JSON(w, http.StatusOK, map[string]any{
|
|
"user": u,
|
|
"token": token,
|
|
})
|
|
}
|
|
|
|
// Me — GET /api/me (نیازمند JWT)
|
|
func (h *Handler) Me(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := UserID(r.Context())
|
|
if !ok {
|
|
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
u, err := h.users.FindByID(r.Context(), id)
|
|
if err != nil {
|
|
httpx.Error(w, http.StatusNotFound, "user not found")
|
|
return
|
|
}
|
|
httpx.JSON(w, http.StatusOK, map[string]any{"user": u})
|
|
}
|