From 63bf1e63b51792ebbb3d9c3dfb73d8a613b2132c Mon Sep 17 00:00:00 2001 From: grendervill Date: Sat, 19 Sep 2026 23:33:59 +0300 Subject: [PATCH] =?UTF-8?q?fix(invites):=20=D0=BA=D0=BE=D0=B4=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=B3=D0=BB=D0=B0=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=BF=D0=BE=D0=B4=D0=BC=D0=B5=D0=BD=D1=8F=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D1=81=D0=BB=D0=B0=D0=B1=D1=8B=D0=BC=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7=D0=B5=20cry?= =?UTF-8?q?pto/rand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вместо предсказуемого запасного кода ручка возвращает 500: приглашение — это доступ к серверу, маскировать отказ источника энтропии нельзя. --- internal/server/api_invites.go | 18 ++--- web/src/components/ui/Menu.tsx | 2 +- web/src/i18n/locales/en.json | 68 +++++++++++++++++++ .../pages/settings/GuildMembersSection.tsx | 4 +- .../pages/settings/ServersSettingsPage.tsx | 5 +- 5 files changed, 84 insertions(+), 13 deletions(-) diff --git a/internal/server/api_invites.go b/internal/server/api_invites.go index 81c30ac..fd81568 100644 --- a/internal/server/api_invites.go +++ b/internal/server/api_invites.go @@ -93,8 +93,12 @@ func (s *Server) registerInviteRoutes(api huma.API) { if err != nil { return nil, err } + code, err := newInviteCode() + if err != nil { + return nil, humaErrorStatus(http.StatusInternalServerError, "internal.error", "cannot generate invite code") + } params := store.CreateInviteParams{ - Code: newInviteCode(), + Code: code, GuildID: guildID, CreatorID: user.ID, MaxUses: input.Body.MaxUses, @@ -359,20 +363,18 @@ func (s *Server) inviteOutput(ctx context.Context, user *store.User, invite *sto } // newInviteCode генерирует код приглашения криптографическим источником. -func newInviteCode() string { +// Ошибка источника энтропии не маскируется слабым кодом: приглашение — +// это доступ к серверу, поэтому лучше вернуть ошибку (AGENT.md 9.2). +func newInviteCode() (string, error) { const length = 10 buf := make([]byte, length) if _, err := rand.Read(buf); err != nil { - // Откат на время: код остаётся уникальным за счёт монотонности. - seed := uint64(time.Now().UnixNano()) //nolint:gosec // не криптография: запасной путь при отказе rand - for i := range buf { - buf[i] = byte(seed >> (uint(i) * 5)) - } + return "", err } var builder strings.Builder builder.Grow(length) for _, value := range buf { builder.WriteByte(inviteCodeAlphabet[int(value)%len(inviteCodeAlphabet)]) } - return builder.String() + return builder.String(), nil } diff --git a/web/src/components/ui/Menu.tsx b/web/src/components/ui/Menu.tsx index d8bd555..281769e 100644 --- a/web/src/components/ui/Menu.tsx +++ b/web/src/components/ui/Menu.tsx @@ -53,7 +53,7 @@ export function Menu({ open, label, onClose, children, anchorRef, className = '' if (!open) { return; } - const onPointerDown = (event: MouseEvent): void => { + const onPointerDown = (event: Event): void => { const target = event.target; if (!(target instanceof Node)) { return; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index c8f8421..4a50ccf 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -183,6 +183,7 @@ "empty": "This server has no visible channels yet.", "select": "Open channel {{name}}", "add": "Create a channel", + "settings": "Server settings", "create": { "title": "New channel", "name": "Channel name", @@ -214,6 +215,13 @@ "user": { "panelLabel": "User panel", "settings": "User settings", + "menu": { + "label": "User menu", + "open": "Open the user menu", + "account": "Account settings", + "guilds": "Server settings", + "status": "Status" + }, "logout": "Sign out", "loggingOut": "Signing out…", "statusLabel": "Status: {{status}}", @@ -240,6 +248,11 @@ "settings": { "title": "Settings", "back": "Back to app", + "switcher": { + "label": "Settings sections", + "account": "Server settings", + "servers": "My servers" + }, "tabs": { "label": "Settings sections", "profile": "Profile", @@ -331,6 +344,61 @@ "memberCount": "{{count}} members", "adminBadge": "admin", "mainBadge": "main" + }, + "servers": { + "pickerLabel": "My servers", + "empty": "You have no servers yet.", + "emptyTitle": "No servers yet", + "emptyHint": "Join the instance main server or create your own — its settings will show up here.", + "select": "Open settings of the server {{name}}", + "notFound": "The server was not found among the ones you can access.", + "readOnlyHint": "Only the owner and members with ADMINISTRATOR or MANAGE_GUILD can edit this.", + "general": { + "title": "General", + "description": "Every member sees the server name and description.", + "name": "Server name", + "guildDescription": "Description", + "descriptionPlaceholder": "What this server is about", + "saved": "Server settings saved." + }, + "nickname": { + "title": "My profile on this server", + "description": "The nickname is visible on this server only.", + "label": "Nickname", + "placeholder": "Leave empty to reset the nickname", + "current": "Current nickname:", + "none": "not set", + "saved": "Nickname saved." + }, + "members": { + "title": "Members", + "description": "The first {{limit}} members of the server.", + "empty": "No members found.", + "roles": "Roles: {{roles}}", + "more": "The full member list with roles arrives in the next phases." + }, + "roles": { + "title": "Roles", + "description": "Role colour and position. Role management arrives in the next phases.", + "empty": "This server has no roles yet.", + "position": "Position {{position}}", + "default": "default" + }, + "danger": { + "title": "Danger zone", + "description": "These actions cannot be undone.", + "leaveOwnerHint": "The owner cannot leave their own server: transfer ownership or delete the server first.", + "leaveHint": "Leave the server and lose access to its channels.", + "leave": "Leave server", + "leaveConfirmTitle": "Leave the server?", + "leaveConfirmBody": "You will leave “{{name}}”. You can come back with an invite.", + "leaveConfirm": "Leave", + "deleteHint": "Delete the server together with its channels, roles and members.", + "delete": "Delete server", + "deleteConfirmTitle": "Delete the server?", + "deleteConfirmBody": "“{{name}}” will be deleted permanently.", + "deleteConfirm": "Delete" + } } } } diff --git a/web/src/pages/settings/GuildMembersSection.tsx b/web/src/pages/settings/GuildMembersSection.tsx index 3afacc5..0bde782 100644 --- a/web/src/pages/settings/GuildMembersSection.tsx +++ b/web/src/pages/settings/GuildMembersSection.tsx @@ -30,7 +30,7 @@ export function GuildMembersSection({ return ( {isPending ? ( @@ -65,7 +65,7 @@ export function GuildMembersSection({ - {t('settings.servers.members.roles', { count: member.role_ids.length })} + {t('settings.servers.members.roles', { roles: member.role_ids.length })} ); diff --git a/web/src/pages/settings/ServersSettingsPage.tsx b/web/src/pages/settings/ServersSettingsPage.tsx index 1731bb9..aca575c 100644 --- a/web/src/pages/settings/ServersSettingsPage.tsx +++ b/web/src/pages/settings/ServersSettingsPage.tsx @@ -90,17 +90,18 @@ export default function ServersSettingsPage() { }); const guild = detail.data; + const sessionGuild = sessionGuilds.find((item) => item.id === activeId) ?? null; const myPermissions = guild?.my_permissions ?? active?.my_permissions ?? []; const ownerId = guild?.owner_id ?? active?.owner_id ?? ''; const canManage = canManageGuildSettings(myPermissions, ownerId, currentUser.data?.id); const currentNickname = useMemo(() => { - const fromSnapshot = active?.my_nickname; + const fromSnapshot = sessionGuild?.my_nickname; if (fromSnapshot !== undefined && fromSnapshot !== '') { return fromSnapshot; } const mine = members.data?.find((member) => member.user_id === currentUser.data?.id); return mine?.nickname ?? ''; - }, [active, members.data, currentUser.data?.id]); + }, [sessionGuild, members.data, currentUser.data?.id]); return (