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 (