v0.1.28: email-идентичность — детерминированные креды из email, credentials-эндпоинт, UI модалка
This commit is contained in:
+30
-6
@@ -155,6 +155,7 @@ func (h *Handler) RegisterPublicRoutes(r *mux.Router) {
|
||||
// jwtMiddleware пропускает /ui/api/auth — единственный публичный endpoint
|
||||
ui.Use(h.jwtMiddleware)
|
||||
ui.HandleFunc("/auth", h.jwtAuth).Methods("POST")
|
||||
ui.HandleFunc("/credentials", h.uiCredentials).Methods("GET")
|
||||
ui.HandleFunc("/health", h.detailedHealth).Methods("GET")
|
||||
ui.HandleFunc("/tenants", h.listTenants).Methods("GET")
|
||||
ui.HandleFunc("/tenants", h.createTenant).Methods("POST")
|
||||
@@ -221,6 +222,14 @@ func (h *Handler) jwtAuth(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Email — единственный ключ идентичности. Без него не создаём тенанта
|
||||
// (иначе hash("") даст один тенант на всех пользователей без email).
|
||||
if claims.Email == "" {
|
||||
log.Warnf("jwt auth: token for sub=%s has no email claim", claims.Sub)
|
||||
jsonErr(w, http.StatusBadRequest, "token has no email claim")
|
||||
return
|
||||
}
|
||||
|
||||
// Валидируем через nubes API
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
@@ -230,9 +239,9 @@ func (h *Handler) jwtAuth(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-provisioning: создаём тенанта если не существует
|
||||
tenantID := auth.TenantIDFromSub(claims.Sub)
|
||||
t, err := h.store.CreateFromJWT(tenantID, claims.Sub, claims.Email, 10)
|
||||
// Auto-provisioning: тенант и ключи детерминированы из email.
|
||||
// Токен — только аутентификация; ключи в ответ не возвращаем (GET /ui/api/credentials).
|
||||
t, err := h.store.CreateFromJWT(claims.Sub, claims.Email, 10)
|
||||
if err != nil {
|
||||
log.Errorf("jwt auth: failed to create tenant: %v", err)
|
||||
jsonErr(w, http.StatusInternalServerError, "failed to provision tenant")
|
||||
@@ -245,13 +254,28 @@ func (h *Handler) jwtAuth(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"email": claims.Email,
|
||||
"tenant_id": t.ID,
|
||||
"access_key": t.AccessKey,
|
||||
"secret_key": t.SecretKey,
|
||||
"max_queues": t.MaxQueues,
|
||||
"token": req.Token, // возвращаем для использования в последующих запросах
|
||||
})
|
||||
}
|
||||
|
||||
// uiCredentials — GET /ui/api/credentials: отдаёт AccessKey/SecretKey текущего тенанта.
|
||||
// Ключи НЕ светятся в других ответах UI API — только по явному запросу под JWT-сессией.
|
||||
func (h *Handler) uiCredentials(w http.ResponseWriter, r *http.Request) {
|
||||
t, ok := currentUITenant(r)
|
||||
if !ok {
|
||||
jsonErr(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"tenant_id": t.ID,
|
||||
"email": t.Email,
|
||||
"access_key": t.AccessKey,
|
||||
"secret_key": t.SecretKey,
|
||||
})
|
||||
}
|
||||
|
||||
// jwtMiddleware — middleware для /ui/api/* endpoints.
|
||||
// Пропускает /ui/api/auth (публичный endpoint авторизации).
|
||||
// Проверяет Authorization: Bearer <jwt> заголовок.
|
||||
@@ -306,7 +330,7 @@ func (h *Handler) jwtMiddleware(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// Проверяем что тенант существует (был создан при /ui/api/auth)
|
||||
jwtTenant, ok := h.store.GetBySub(claims.Sub)
|
||||
jwtTenant, ok := h.store.GetByEmail(claims.Email)
|
||||
if !ok {
|
||||
jsonErr(w, http.StatusForbidden, "tenant not found — authenticate first via POST /ui/api/auth")
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user