- Media (voice/video/image/file): private bucket, presigned PUT upload, reference-only publish, membership-gated presigned GET for viewing - Avatars: public bucket, presigned PUT + direct public URL, profile.update event - messages.media_key + media_meta (migration 1720000000005_media) - Explicit MinIO region on presigning client (avoids getBucketRegion network call) - Event type values sourced from EventType constants (no raw string literals) - Web: attach button, MediaView renderer, avatar upload in topbar Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFRyKxkKEjfAgpXygzoNiD
217 lines
5.8 KiB
TypeScript
217 lines
5.8 KiB
TypeScript
// Response JSON Schemas mirroring the core types. Shared by the backend for
|
|
// OpenAPI docs and response serialization, so the documented contract cannot
|
|
// drift from what the API returns.
|
|
|
|
import { mediaRefSchema } from './media';
|
|
|
|
export const publicUserSchema = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['id', 'username', 'displayName', 'avatarUrl'],
|
|
properties: {
|
|
id: { type: 'string', format: 'uuid' },
|
|
username: { type: 'string' },
|
|
displayName: { type: 'string' },
|
|
avatarUrl: { type: ['string', 'null'] },
|
|
},
|
|
} as const;
|
|
|
|
export const userSchema = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: [
|
|
'id',
|
|
'username',
|
|
'displayName',
|
|
'avatarUrl',
|
|
'email',
|
|
'phone',
|
|
'createdAt',
|
|
'lastSeenAt',
|
|
],
|
|
properties: {
|
|
id: { type: 'string', format: 'uuid' },
|
|
username: { type: 'string' },
|
|
displayName: { type: 'string' },
|
|
avatarUrl: { type: ['string', 'null'] },
|
|
email: { type: ['string', 'null'] },
|
|
phone: { type: ['string', 'null'] },
|
|
createdAt: { type: 'string', format: 'date-time' },
|
|
lastSeenAt: { type: ['string', 'null'], format: 'date-time' },
|
|
},
|
|
} as const;
|
|
|
|
// The refresh token is delivered as an httpOnly cookie, so it is deliberately
|
|
// absent from the response body.
|
|
export const authResultSchema = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['user', 'accessToken', 'accessTokenExpiresIn'],
|
|
properties: {
|
|
user: userSchema,
|
|
accessToken: { type: 'string' },
|
|
accessTokenExpiresIn: { type: 'integer' },
|
|
},
|
|
} as const;
|
|
|
|
export const sessionSchema = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['id', 'userAgent', 'ip', 'createdAt', 'lastUsedAt', 'current'],
|
|
properties: {
|
|
id: { type: 'string', format: 'uuid' },
|
|
userAgent: { type: ['string', 'null'] },
|
|
ip: { type: ['string', 'null'] },
|
|
createdAt: { type: 'string', format: 'date-time' },
|
|
lastUsedAt: { type: 'string', format: 'date-time' },
|
|
current: { type: 'boolean' },
|
|
},
|
|
} as const;
|
|
|
|
export const sessionListSchema = {
|
|
type: 'array',
|
|
items: sessionSchema,
|
|
} as const;
|
|
|
|
export const centrifugoTokenSchema = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['token', 'expiresIn'],
|
|
properties: {
|
|
token: { type: 'string' },
|
|
expiresIn: { type: 'integer' },
|
|
},
|
|
} as const;
|
|
|
|
export const errorSchema = {
|
|
type: 'object',
|
|
required: ['error'],
|
|
properties: {
|
|
error: { type: 'string' },
|
|
message: { type: 'string' },
|
|
},
|
|
} as const;
|
|
|
|
export const publicUserListSchema = { type: 'array', items: publicUserSchema } as const;
|
|
|
|
export const conversationSchema = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: [
|
|
'id',
|
|
'type',
|
|
'title',
|
|
'peer',
|
|
'createdBy',
|
|
'createdAt',
|
|
'lastMessageAt',
|
|
'unreadCount',
|
|
],
|
|
properties: {
|
|
id: { type: 'string', format: 'uuid' },
|
|
type: { type: 'string', enum: ['direct', 'group'] },
|
|
title: { type: ['string', 'null'] },
|
|
peer: {
|
|
oneOf: [publicUserSchema, { type: 'null' }],
|
|
},
|
|
createdBy: { type: 'string', format: 'uuid' },
|
|
createdAt: { type: 'string', format: 'date-time' },
|
|
lastMessageAt: { type: 'string', format: 'date-time' },
|
|
unreadCount: { type: 'integer' },
|
|
},
|
|
} as const;
|
|
|
|
export const reactionSummarySchema = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['emoji', 'count', 'mine'],
|
|
properties: {
|
|
emoji: { type: 'string' },
|
|
count: { type: 'integer' },
|
|
mine: { type: 'boolean' },
|
|
},
|
|
} as const;
|
|
|
|
export const presenceSchema = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['userId', 'online', 'lastSeenAt'],
|
|
properties: {
|
|
userId: { type: 'string', format: 'uuid' },
|
|
online: { type: 'boolean' },
|
|
lastSeenAt: { type: ['string', 'null'], format: 'date-time' },
|
|
},
|
|
} as const;
|
|
|
|
export const presenceListSchema = { type: 'array', items: presenceSchema } as const;
|
|
|
|
export const conversationListSchema = { type: 'array', items: conversationSchema } as const;
|
|
|
|
export const conversationMemberSchema = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['userId', 'role', 'joinedAt', 'user'],
|
|
properties: {
|
|
userId: { type: 'string', format: 'uuid' },
|
|
role: { type: 'string' },
|
|
joinedAt: { type: 'string', format: 'date-time' },
|
|
user: publicUserSchema,
|
|
},
|
|
} as const;
|
|
|
|
export const conversationMemberListSchema = {
|
|
type: 'array',
|
|
items: conversationMemberSchema,
|
|
} as const;
|
|
|
|
export const contactSchema = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['userId', 'user', 'createdAt'],
|
|
properties: {
|
|
userId: { type: 'string', format: 'uuid' },
|
|
user: publicUserSchema,
|
|
createdAt: { type: 'string', format: 'date-time' },
|
|
},
|
|
} as const;
|
|
|
|
export const contactListSchema = { type: 'array', items: contactSchema } as const;
|
|
|
|
export const messageSchema = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: [
|
|
'id',
|
|
'conversationId',
|
|
'senderId',
|
|
'sender',
|
|
'content',
|
|
'contentType',
|
|
'encryption',
|
|
'clientMsgId',
|
|
'seq',
|
|
'createdAt',
|
|
'editedAt',
|
|
'deletedAt',
|
|
'reactions',
|
|
'media',
|
|
],
|
|
properties: {
|
|
id: { type: 'string', format: 'uuid' },
|
|
conversationId: { type: 'string', format: 'uuid' },
|
|
senderId: { type: 'string', format: 'uuid' },
|
|
sender: publicUserSchema,
|
|
content: { type: 'string' },
|
|
contentType: { type: 'string' },
|
|
encryption: { type: ['string', 'null'] },
|
|
clientMsgId: { type: 'string' },
|
|
seq: { type: 'integer' },
|
|
createdAt: { type: 'string', format: 'date-time' },
|
|
editedAt: { type: ['string', 'null'], format: 'date-time' },
|
|
deletedAt: { type: ['string', 'null'], format: 'date-time' },
|
|
reactions: { type: 'array', items: reactionSummarySchema },
|
|
media: { oneOf: [mediaRefSchema, { type: 'null' }] },
|
|
},
|
|
} as const;
|
|
|
|
export const messageListSchema = { type: 'array', items: messageSchema } as const;
|