fix(invites): код приглашения не подменяется слабым при отказе crypto/rand
Вместо предсказуемого запасного кода ручка возвращает 500: приглашение — это доступ к серверу, маскировать отказ источника энтропии нельзя.
This commit is contained in:
@@ -93,8 +93,12 @@ func (s *Server) registerInviteRoutes(api huma.API) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
code, err := newInviteCode()
|
||||||
|
if err != nil {
|
||||||
|
return nil, humaErrorStatus(http.StatusInternalServerError, "internal.error", "cannot generate invite code")
|
||||||
|
}
|
||||||
params := store.CreateInviteParams{
|
params := store.CreateInviteParams{
|
||||||
Code: newInviteCode(),
|
Code: code,
|
||||||
GuildID: guildID,
|
GuildID: guildID,
|
||||||
CreatorID: user.ID,
|
CreatorID: user.ID,
|
||||||
MaxUses: input.Body.MaxUses,
|
MaxUses: input.Body.MaxUses,
|
||||||
@@ -359,20 +363,18 @@ func (s *Server) inviteOutput(ctx context.Context, user *store.User, invite *sto
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newInviteCode генерирует код приглашения криптографическим источником.
|
// newInviteCode генерирует код приглашения криптографическим источником.
|
||||||
func newInviteCode() string {
|
// Ошибка источника энтропии не маскируется слабым кодом: приглашение —
|
||||||
|
// это доступ к серверу, поэтому лучше вернуть ошибку (AGENT.md 9.2).
|
||||||
|
func newInviteCode() (string, error) {
|
||||||
const length = 10
|
const length = 10
|
||||||
buf := make([]byte, length)
|
buf := make([]byte, length)
|
||||||
if _, err := rand.Read(buf); err != nil {
|
if _, err := rand.Read(buf); err != nil {
|
||||||
// Откат на время: код остаётся уникальным за счёт монотонности.
|
return "", err
|
||||||
seed := uint64(time.Now().UnixNano()) //nolint:gosec // не криптография: запасной путь при отказе rand
|
|
||||||
for i := range buf {
|
|
||||||
buf[i] = byte(seed >> (uint(i) * 5))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.Grow(length)
|
builder.Grow(length)
|
||||||
for _, value := range buf {
|
for _, value := range buf {
|
||||||
builder.WriteByte(inviteCodeAlphabet[int(value)%len(inviteCodeAlphabet)])
|
builder.WriteByte(inviteCodeAlphabet[int(value)%len(inviteCodeAlphabet)])
|
||||||
}
|
}
|
||||||
return builder.String()
|
return builder.String(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function Menu({ open, label, onClose, children, anchorRef, className = ''
|
|||||||
if (!open) {
|
if (!open) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const onPointerDown = (event: MouseEvent): void => {
|
const onPointerDown = (event: Event): void => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!(target instanceof Node)) {
|
if (!(target instanceof Node)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -183,6 +183,7 @@
|
|||||||
"empty": "This server has no visible channels yet.",
|
"empty": "This server has no visible channels yet.",
|
||||||
"select": "Open channel {{name}}",
|
"select": "Open channel {{name}}",
|
||||||
"add": "Create a channel",
|
"add": "Create a channel",
|
||||||
|
"settings": "Server settings",
|
||||||
"create": {
|
"create": {
|
||||||
"title": "New channel",
|
"title": "New channel",
|
||||||
"name": "Channel name",
|
"name": "Channel name",
|
||||||
@@ -214,6 +215,13 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"panelLabel": "User panel",
|
"panelLabel": "User panel",
|
||||||
"settings": "User settings",
|
"settings": "User settings",
|
||||||
|
"menu": {
|
||||||
|
"label": "User menu",
|
||||||
|
"open": "Open the user menu",
|
||||||
|
"account": "Account settings",
|
||||||
|
"guilds": "Server settings",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
"logout": "Sign out",
|
"logout": "Sign out",
|
||||||
"loggingOut": "Signing out…",
|
"loggingOut": "Signing out…",
|
||||||
"statusLabel": "Status: {{status}}",
|
"statusLabel": "Status: {{status}}",
|
||||||
@@ -240,6 +248,11 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"back": "Back to app",
|
"back": "Back to app",
|
||||||
|
"switcher": {
|
||||||
|
"label": "Settings sections",
|
||||||
|
"account": "Server settings",
|
||||||
|
"servers": "My servers"
|
||||||
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"label": "Settings sections",
|
"label": "Settings sections",
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
@@ -331,6 +344,61 @@
|
|||||||
"memberCount": "{{count}} members",
|
"memberCount": "{{count}} members",
|
||||||
"adminBadge": "admin",
|
"adminBadge": "admin",
|
||||||
"mainBadge": "main"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function GuildMembersSection({
|
|||||||
return (
|
return (
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
title={t('settings.servers.members.title')}
|
title={t('settings.servers.members.title')}
|
||||||
description={t('settings.servers.members.description', { count: PREVIEW_LIMIT })}
|
description={t('settings.servers.members.description', { limit: PREVIEW_LIMIT })}
|
||||||
>
|
>
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<SkeletonLines className="mt-4" rows={4} />
|
<SkeletonLines className="mt-4" rows={4} />
|
||||||
@@ -65,7 +65,7 @@ export function GuildMembersSection({
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<Badge>
|
<Badge>
|
||||||
{t('settings.servers.members.roles', { count: member.role_ids.length })}
|
{t('settings.servers.members.roles', { roles: member.role_ids.length })}
|
||||||
</Badge>
|
</Badge>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,17 +90,18 @@ export default function ServersSettingsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const guild = detail.data;
|
const guild = detail.data;
|
||||||
|
const sessionGuild = sessionGuilds.find((item) => item.id === activeId) ?? null;
|
||||||
const myPermissions = guild?.my_permissions ?? active?.my_permissions ?? [];
|
const myPermissions = guild?.my_permissions ?? active?.my_permissions ?? [];
|
||||||
const ownerId = guild?.owner_id ?? active?.owner_id ?? '';
|
const ownerId = guild?.owner_id ?? active?.owner_id ?? '';
|
||||||
const canManage = canManageGuildSettings(myPermissions, ownerId, currentUser.data?.id);
|
const canManage = canManageGuildSettings(myPermissions, ownerId, currentUser.data?.id);
|
||||||
const currentNickname = useMemo(() => {
|
const currentNickname = useMemo(() => {
|
||||||
const fromSnapshot = active?.my_nickname;
|
const fromSnapshot = sessionGuild?.my_nickname;
|
||||||
if (fromSnapshot !== undefined && fromSnapshot !== '') {
|
if (fromSnapshot !== undefined && fromSnapshot !== '') {
|
||||||
return fromSnapshot;
|
return fromSnapshot;
|
||||||
}
|
}
|
||||||
const mine = members.data?.find((member) => member.user_id === currentUser.data?.id);
|
const mine = members.data?.find((member) => member.user_id === currentUser.data?.id);
|
||||||
return mine?.nickname ?? '';
|
return mine?.nickname ?? '';
|
||||||
}, [active, members.data, currentUser.data?.id]);
|
}, [sessionGuild, members.data, currentUser.data?.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
|
|||||||
Reference in New Issue
Block a user