fix(files): аватары и баннеры доступны авторизованным пользователям
Аватар нельзя было показать в списке друзей: файл без комнаты был доступен только загрузившему. Добавлено назначение файла (`files.purpose`: attachment | avatar | banner, миграция 00006): аватары и баннеры отдаются любому авторизованному пользователю, вложения по-прежнему проверяются по правам комнаты, а непривязанные загрузки остаются приватными.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
-- +goose Up
|
||||
-- Назначение файла: вложения видны по правам комнаты, аватары и баннеры —
|
||||
-- всем авторизованным (AGENT.md 7.2, 7.7).
|
||||
ALTER TABLE files ADD COLUMN purpose TEXT NOT NULL DEFAULT 'attachment';
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE files DROP COLUMN purpose;
|
||||
@@ -130,6 +130,7 @@ func (s *Server) handleAvatarUpload(w http.ResponseWriter, r *http.Request) {
|
||||
UploaderID: ¤tUser.ID,
|
||||
Filename: sanitizeFilename(header.Filename),
|
||||
ContentType: contentType,
|
||||
Purpose: "avatar",
|
||||
}, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
writeHumaAPIError(w, err)
|
||||
@@ -295,6 +296,11 @@ func (s *Server) requireFileAccess(ctx context.Context, rawFileID string, user *
|
||||
if err != nil {
|
||||
return nil, humaError(err)
|
||||
}
|
||||
// Аватары и баннеры видны всем авторизованным: они показываются в списках
|
||||
// участников и друзей (AGENT.md 7.2).
|
||||
if file.Purpose == "avatar" || file.Purpose == "banner" {
|
||||
return file, nil
|
||||
}
|
||||
if file.ChannelID != nil {
|
||||
if _, _, _, err := s.requireChannelPermission(ctx, formatSnowflake(*file.ChannelID), user, permissions.ViewChannel); err != nil {
|
||||
return nil, err
|
||||
@@ -355,6 +361,7 @@ func (s *Server) saveUpload(ctx context.Context, file store.File, content io.Rea
|
||||
SizeBytes: written,
|
||||
StoragePath: path,
|
||||
SHA256: hex.EncodeToString(hasher.Sum(nil)),
|
||||
Purpose: file.Purpose,
|
||||
})
|
||||
if err != nil {
|
||||
_ = os.Remove(path)
|
||||
|
||||
@@ -254,6 +254,25 @@ func TestTimezoneAndAvatarUpdate(t *testing.T) {
|
||||
t.Fatalf("avatar_file_id = %q, want %q", profile.User.AvatarFileID, fileID)
|
||||
}
|
||||
|
||||
// Аватар виден другому авторизованному пользователю (он показывается в
|
||||
// списках друзей и участников), но не анонимному.
|
||||
otherCookie := registerAndLogin(t, srv, "tz_other", "tz-other@example.com")
|
||||
_ = otherCookie
|
||||
downloadReq, err := http.NewRequestWithContext(t.Context(), http.MethodGet,
|
||||
httpServer.URL+"/files/"+fileID, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
downloadReq.AddCookie(otherCookie)
|
||||
downloadResp, err := httpServer.Client().Do(downloadReq)
|
||||
if err != nil {
|
||||
t.Fatalf("avatar download: %v", err)
|
||||
}
|
||||
_ = downloadResp.Body.Close()
|
||||
if downloadResp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("другой пользователь не видит аватар: %d", downloadResp.StatusCode)
|
||||
}
|
||||
|
||||
removed := doJSON(t, srv, http.MethodDelete, "/api/v1/users/@me/avatar", "", cookie)
|
||||
if removed.Code != http.StatusOK {
|
||||
t.Fatalf("delete avatar = %d, body = %s", removed.Code, removed.Body.String())
|
||||
|
||||
+13
-6
@@ -20,7 +20,10 @@ type File struct {
|
||||
Height int
|
||||
StoragePath string
|
||||
SHA256 string
|
||||
CreatedAt time.Time
|
||||
// Purpose: attachment (по умолчанию) | avatar | banner — от него зависит
|
||||
// проверка доступа при выдаче файла.
|
||||
Purpose string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// CreateFileParams — параметры регистрации файла.
|
||||
@@ -36,23 +39,27 @@ type CreateFileParams struct {
|
||||
Height int
|
||||
StoragePath string
|
||||
SHA256 string
|
||||
Purpose string
|
||||
}
|
||||
|
||||
const fileColumns = `id, uploader_id, guild_id, channel_id, message_id, filename,
|
||||
content_type, size_bytes, width, height, storage_path, sha256, created_at`
|
||||
content_type, size_bytes, width, height, storage_path, sha256, created_at, purpose`
|
||||
|
||||
// CreateFile регистрирует загруженный файл.
|
||||
func (s *Store) CreateFile(ctx context.Context, params CreateFileParams) (*File, error) {
|
||||
if params.ID == 0 {
|
||||
params.ID = s.NextID()
|
||||
}
|
||||
if params.Purpose == "" {
|
||||
params.Purpose = "attachment"
|
||||
}
|
||||
_, err := s.writer.ExecContext(ctx, `
|
||||
INSERT INTO files (id, uploader_id, guild_id, channel_id, message_id, filename,
|
||||
content_type, size_bytes, width, height, storage_path, sha256, created_at)
|
||||
VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
content_type, size_bytes, width, height, storage_path, sha256, created_at, purpose)
|
||||
VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
int64(params.ID), int64(params.UploaderID), nullableID(params.GuildID), nullableID(params.ChannelID),
|
||||
params.Filename, params.ContentType, params.SizeBytes, params.Width, params.Height,
|
||||
params.StoragePath, params.SHA256, s.Now())
|
||||
params.StoragePath, params.SHA256, s.Now(), params.Purpose)
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
@@ -126,7 +133,7 @@ func scanFile(scanner interface{ Scan(...any) error }) (*File, error) {
|
||||
)
|
||||
err := scanner.Scan(&file.ID, &uploaderID, &guildID, &channelID, &messageID, &file.Filename,
|
||||
&file.ContentType, &file.SizeBytes, &file.Width, &file.Height, &file.StoragePath,
|
||||
&file.SHA256, &createdAt)
|
||||
&file.SHA256, &createdAt, &file.Purpose)
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user