package server import ( "context" "encoding/json" "errors" "net/http" "strconv" "github.com/danielgtaylor/huma/v2" "glchat/internal/permissions" "glchat/internal/store" ) // sessionContextValue — сессия, восстановленная из cookie или Bearer-токена. type sessionContextValue struct { User *store.User Session *store.Session } type sessionContextKey struct{} // sessionContext резолвит сессию один раз на запрос и кладёт её в контекст. // Отсутствие сессии не ошибка: часть ручек публичная (мета, OpenAPI). func (s *Server) sessionContext(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if s.auth == nil { next.ServeHTTP(w, r) return } token := sessionToken(r) if token == "" { next.ServeHTTP(w, r) return } user, session, err := s.auth.ResolveSession(r.Context(), token) if err != nil { next.ServeHTTP(w, r) return } ctx := context.WithValue(r.Context(), sessionContextKey{}, &sessionContextValue{User: user, Session: session}) next.ServeHTTP(w, r.WithContext(ctx)) }) } // sessionToken читает токен сессии из cookie (браузер) или Bearer (desktop). func sessionToken(r *http.Request) string { if cookie, err := r.Cookie(sessionCookieName); err == nil && cookie.Value != "" { return cookie.Value } return normalizeBearer(r.Header.Get("Authorization")) } func sessionFromContext(ctx context.Context) (*store.User, *store.Session, bool) { value, ok := ctx.Value(sessionContextKey{}).(*sessionContextValue) if !ok || value.User == nil || value.Session == nil { return nil, nil, false } return value.User, value.Session, true } // requireUser достаёт текущего пользователя или возвращает 401 в конверте API. func requireUser(ctx context.Context) (*store.User, *store.Session, error) { user, session, ok := sessionFromContext(ctx) if !ok { return nil, nil, humaErrorStatus(http.StatusUnauthorized, "auth.session_expired", "authentication required") } return user, session, nil } // requireInstanceAdmin проверяет права администратора инстанса (AGENT.md 6.5). func requireInstanceAdmin(ctx context.Context) (*store.User, error) { user, _, err := requireUser(ctx) if err != nil { return nil, err } if !user.IsInstanceAdmin { return nil, humaErrorStatus(http.StatusForbidden, "instance.admin_required", "instance administrator rights required") } return user, nil } // humaAPIError — ошибка huma в едином конверте {"error":{"code","message"}} // (AGENT.md 8.5): клиент разбирает код, а не текст. type humaAPIError struct { status int code string message string } func (e *humaAPIError) Error() string { return e.code + ": " + e.message } func (e *humaAPIError) GetStatus() int { return e.status } func (e *humaAPIError) MarshalJSON() ([]byte, error) { return json.Marshal(map[string]any{ "error": map[string]any{"code": e.code, "message": e.message}, }) } // humaErrorStatus создаёт ошибку с явным кодом. func humaErrorStatus(status int, code, message string) huma.StatusError { return &humaAPIError{status: status, code: code, message: message} } // humaError переводит доменную ошибку в ответ API с тем же кодом, что и // chi-ручки (единая таблица в errors.go). func humaError(err error) huma.StatusError { if err == nil { return nil } converted := newAPIError(err) return &humaAPIError{status: converted.Status, code: converted.Code, message: converted.Message} } // parseID разбирает Snowflake из пути: идентификаторы в API — строки (AGENT.md 6.4). func parseID(name, value string) (uint64, error) { if value == "" { return 0, humaErrorStatus(http.StatusBadRequest, "request.bad", name+" is required") } parsed, err := strconv.ParseUint(value, 10, 64) if err != nil { return 0, humaErrorStatus(http.StatusBadRequest, "request.bad", name+" must be a snowflake id") } return parsed, nil } // guildPermissions считает права пользователя на сервере. func (s *Server) guildPermissions(ctx context.Context, guildID uint64, user *store.User) (permissions.Resolved, error) { resolved, err := s.perms.Guild(ctx, guildID, user.ID, user.IsInstanceAdmin) if err != nil { if errors.Is(err, store.ErrNotFound) { return permissions.Resolved{}, humaErrorStatus(http.StatusNotFound, "not_found", "guild not found") } return permissions.Resolved{}, humaError(err) } if !resolved.Has(permissions.ViewGuild) { // Существование сервера не подтверждаем: 404 вместо 403 (AGENT.md 9.7). return permissions.Resolved{}, humaErrorStatus(http.StatusNotFound, "not_found", "guild not found") } return resolved, nil } // requireGuildPermission считает права и требует конкретное разрешение. func (s *Server) requireGuildPermission(ctx context.Context, guildID string, user *store.User, permission permissions.Permission) (uint64, permissions.Resolved, error) { id, err := parseID("guild_id", guildID) if err != nil { return 0, permissions.Resolved{}, err } resolved, err := s.guildPermissions(ctx, id, user) if err != nil { return 0, permissions.Resolved{}, err } if !resolved.Has(permission) { return 0, permissions.Resolved{}, humaErrorStatus(http.StatusForbidden, "perm.denied", "permission denied") } return id, resolved, nil }