вложения, канальчики, бим-бим + бам-бам
This commit is contained in:
@@ -16,7 +16,7 @@ export const useAuth = createGlobalState(() => {
|
|||||||
|
|
||||||
async function login(username: string, password: string): Promise<void> {
|
async function login(username: string, password: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = await chadApi<Me>('/login', {
|
const result = await chadApi<Me>('/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
username,
|
username,
|
||||||
@@ -33,7 +33,7 @@ export const useAuth = createGlobalState(() => {
|
|||||||
|
|
||||||
async function register(username: string, password: string): Promise<void> {
|
async function register(username: string, password: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = await chadApi<Me>('/register', {
|
const result = await chadApi<Me>('/auth/register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
username,
|
username,
|
||||||
@@ -50,7 +50,7 @@ export const useAuth = createGlobalState(() => {
|
|||||||
|
|
||||||
async function logout(): Promise<void> {
|
async function logout(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await chadApi('/logout', { method: 'POST' })
|
await chadApi('/auth/logout', { method: 'POST' })
|
||||||
|
|
||||||
setMe(undefined)
|
setMe(undefined)
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
|
import chadApi from '#shared/chad-api'
|
||||||
import { createGlobalState } from '@vueuse/core'
|
import { createGlobalState } from '@vueuse/core'
|
||||||
|
|
||||||
export interface ChatClientMessage {
|
export interface ChatClientMessage {
|
||||||
text: string
|
text: string
|
||||||
replyTo?: {
|
// replyTo?: {
|
||||||
messageId: string
|
// messageId: string
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
id: string
|
id: string
|
||||||
sender: string
|
senderId: string
|
||||||
text: string
|
text: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
replyTo?: {
|
updatedAt: string
|
||||||
messageId: string
|
attachments: string[]
|
||||||
sender: string
|
// replyTo?: {
|
||||||
text: string
|
// messageId: string
|
||||||
}
|
// sender: string
|
||||||
|
// text: string
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useChat = createGlobalState(() => {
|
export const useChat = createGlobalState(() => {
|
||||||
@@ -41,16 +44,16 @@ export const useChat = createGlobalState(() => {
|
|||||||
})
|
})
|
||||||
}, { immediate: true, flush: 'sync' })
|
}, { immediate: true, flush: 'sync' })
|
||||||
|
|
||||||
function sendMessage(message: ChatClientMessage) {
|
async function sendMessage(message: ChatClientMessage) {
|
||||||
if (!signaling.connected.value)
|
|
||||||
return
|
|
||||||
|
|
||||||
message.text = message.text.trim()
|
message.text = message.text.trim()
|
||||||
|
|
||||||
if (!message.text.length)
|
if (!message.text.length)
|
||||||
return
|
return
|
||||||
|
|
||||||
signaling.socket.value!.emit('chat:message', message)
|
await chadApi<ChatMessage>('/chat/send', {
|
||||||
|
method: 'POST',
|
||||||
|
body: message,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const usePreferences = createGlobalState(() => {
|
|||||||
async ([toggleInputHotkey, toggleOutputHotkey]) => {
|
async ([toggleInputHotkey, toggleOutputHotkey]) => {
|
||||||
try {
|
try {
|
||||||
await chadApi(
|
await chadApi(
|
||||||
'/preferences',
|
'/user/preferences',
|
||||||
{
|
{
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: {
|
body: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
|||||||
|
|
||||||
if (!me.value) {
|
if (!me.value) {
|
||||||
try {
|
try {
|
||||||
setMe(await chadApi('/me', { method: 'GET' }))
|
setMe(await chadApi('/auth/me', { method: 'GET' }))
|
||||||
|
|
||||||
if (to.meta.auth !== false)
|
if (to.meta.auth !== false)
|
||||||
return navigateTo({ name: 'Index' })
|
return navigateTo({ name: 'Index' })
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default defineNuxtRouteMiddleware(async () => {
|
|||||||
return
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const preferences = await chadApi<SyncedPreferences>('/preferences', { method: 'GET' })
|
const preferences = await chadApi<SyncedPreferences>('/user/preferences', { method: 'GET' })
|
||||||
|
|
||||||
if (!preferences)
|
if (!preferences)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,25 +10,35 @@
|
|||||||
:key="message.id"
|
:key="message.id"
|
||||||
class="w-fit max-w-[60%]"
|
class="w-fit max-w-[60%]"
|
||||||
:class="{
|
:class="{
|
||||||
'ml-auto': message.sender === me?.username,
|
'ml-auto': message.senderId === me?.userId,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
v-if="message.sender !== me?.username"
|
v-if="message.senderId !== me?.userId"
|
||||||
class="text-sm text-muted-color mb-1"
|
class="text-sm text-muted-color mb-1"
|
||||||
>
|
>
|
||||||
{{ message.sender }}
|
{{ message.senderId }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="px-3 py-2 rounded-lg"
|
class="px-3 py-2 rounded-lg"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-surface-800': message.sender !== me?.username,
|
'bg-surface-800 rounded-tl': message.senderId !== me?.userId,
|
||||||
'bg-surface-700': message.sender === me?.username,
|
'bg-surface-700 rounded-tr': message.senderId === me?.userId,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<p class="[&>a]:break-all" @click="handleMessageClick" v-html="parseMessageText(message.text)" />
|
<p class="[&>a]:break-all" @click="handleMessageClick" v-html="parseMessageText(message.text)" />
|
||||||
|
|
||||||
|
<div v-if="message.attachments.length > 0" class="flex flex-col gap-2 mt-2">
|
||||||
|
<img
|
||||||
|
v-for="attachmentId in message.attachments"
|
||||||
|
:key="attachmentId"
|
||||||
|
class="rounded-xl max-w-60"
|
||||||
|
:src="`http://localhost:4000/chad/attachment/${attachmentId}`"
|
||||||
|
:alt="attachmentId"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="mt-1 text-right text-sm text-muted-color" :title="formatDate(message.createdAt, 'dd.MM.yyyy, HH:mm')">
|
<p class="mt-1 text-right text-sm text-muted-color" :title="formatDate(message.createdAt, 'dd.MM.yyyy, HH:mm')">
|
||||||
{{ formatDate(message.createdAt) }}
|
{{ formatDate(message.createdAt) }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ async function save() {
|
|||||||
|
|
||||||
saving.value = true
|
saving.value = true
|
||||||
|
|
||||||
const updatedMe = await chadApi('/profile', {
|
const updatedMe = await chadApi('/user/profile', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: {
|
body: {
|
||||||
displayName: displayName.value,
|
displayName: displayName.value,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "Chad",
|
"productName": "Chad",
|
||||||
"version": "0.3.0-rc.2",
|
"version": "0.3.0-rc.3",
|
||||||
"identifier": "xyz.koptilnya.chad",
|
"identifier": "xyz.koptilnya.chad",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../.output/public",
|
"frontendDist": "../.output/public",
|
||||||
|
|||||||
677
server/Api.ts
Normal file
677
server/Api.ts
Normal file
@@ -0,0 +1,677 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* tslint:disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||||
|
* ## ##
|
||||||
|
* ## AUTHOR: acacode ##
|
||||||
|
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type QueryParamsType = Record<string | number, any>;
|
||||||
|
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
|
||||||
|
|
||||||
|
export interface FullRequestParams extends Omit<RequestInit, "body"> {
|
||||||
|
/** set parameter to `true` for call `securityWorker` for this request */
|
||||||
|
secure?: boolean;
|
||||||
|
/** request path */
|
||||||
|
path: string;
|
||||||
|
/** content type of request body */
|
||||||
|
type?: ContentType;
|
||||||
|
/** query params */
|
||||||
|
query?: QueryParamsType;
|
||||||
|
/** format of response (i.e. response.json() -> format: "json") */
|
||||||
|
format?: ResponseFormat;
|
||||||
|
/** request body */
|
||||||
|
body?: unknown;
|
||||||
|
/** base url */
|
||||||
|
baseUrl?: string;
|
||||||
|
/** request cancellation token */
|
||||||
|
cancelToken?: CancelToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestParams = Omit<
|
||||||
|
FullRequestParams,
|
||||||
|
"body" | "method" | "query" | "path"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface ApiConfig<SecurityDataType = unknown> {
|
||||||
|
baseUrl?: string;
|
||||||
|
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
|
||||||
|
securityWorker?: (
|
||||||
|
securityData: SecurityDataType | null,
|
||||||
|
) => Promise<RequestParams | void> | RequestParams | void;
|
||||||
|
customFetch?: typeof fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HttpResponse<D extends unknown, E extends unknown = unknown>
|
||||||
|
extends Response {
|
||||||
|
data: D;
|
||||||
|
error: E;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CancelToken = Symbol | string | number;
|
||||||
|
|
||||||
|
export enum ContentType {
|
||||||
|
Json = "application/json",
|
||||||
|
JsonApi = "application/vnd.api+json",
|
||||||
|
FormData = "multipart/form-data",
|
||||||
|
UrlEncoded = "application/x-www-form-urlencoded",
|
||||||
|
Text = "text/plain",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpClient<SecurityDataType = unknown> {
|
||||||
|
public baseUrl: string = "";
|
||||||
|
private securityData: SecurityDataType | null = null;
|
||||||
|
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
||||||
|
private abortControllers = new Map<CancelToken, AbortController>();
|
||||||
|
private customFetch = (...fetchParams: Parameters<typeof fetch>) =>
|
||||||
|
fetch(...fetchParams);
|
||||||
|
|
||||||
|
private baseApiParams: RequestParams = {
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {},
|
||||||
|
redirect: "follow",
|
||||||
|
referrerPolicy: "no-referrer",
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
|
||||||
|
Object.assign(this, apiConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSecurityData = (data: SecurityDataType | null) => {
|
||||||
|
this.securityData = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected encodeQueryParam(key: string, value: any) {
|
||||||
|
const encodedKey = encodeURIComponent(key);
|
||||||
|
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addQueryParam(query: QueryParamsType, key: string) {
|
||||||
|
return this.encodeQueryParam(key, query[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addArrayQueryParam(query: QueryParamsType, key: string) {
|
||||||
|
const value = query[key];
|
||||||
|
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toQueryString(rawQuery?: QueryParamsType): string {
|
||||||
|
const query = rawQuery || {};
|
||||||
|
const keys = Object.keys(query).filter(
|
||||||
|
(key) => "undefined" !== typeof query[key],
|
||||||
|
);
|
||||||
|
return keys
|
||||||
|
.map((key) =>
|
||||||
|
Array.isArray(query[key])
|
||||||
|
? this.addArrayQueryParam(query, key)
|
||||||
|
: this.addQueryParam(query, key),
|
||||||
|
)
|
||||||
|
.join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addQueryParams(rawQuery?: QueryParamsType): string {
|
||||||
|
const queryString = this.toQueryString(rawQuery);
|
||||||
|
return queryString ? `?${queryString}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private contentFormatters: Record<ContentType, (input: any) => any> = {
|
||||||
|
[ContentType.Json]: (input: any) =>
|
||||||
|
input !== null && (typeof input === "object" || typeof input === "string")
|
||||||
|
? JSON.stringify(input)
|
||||||
|
: input,
|
||||||
|
[ContentType.JsonApi]: (input: any) =>
|
||||||
|
input !== null && (typeof input === "object" || typeof input === "string")
|
||||||
|
? JSON.stringify(input)
|
||||||
|
: input,
|
||||||
|
[ContentType.Text]: (input: any) =>
|
||||||
|
input !== null && typeof input !== "string"
|
||||||
|
? JSON.stringify(input)
|
||||||
|
: input,
|
||||||
|
[ContentType.FormData]: (input: any) => {
|
||||||
|
if (input instanceof FormData) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(input || {}).reduce((formData, key) => {
|
||||||
|
const property = input[key];
|
||||||
|
formData.append(
|
||||||
|
key,
|
||||||
|
property instanceof Blob
|
||||||
|
? property
|
||||||
|
: typeof property === "object" && property !== null
|
||||||
|
? JSON.stringify(property)
|
||||||
|
: `${property}`,
|
||||||
|
);
|
||||||
|
return formData;
|
||||||
|
}, new FormData());
|
||||||
|
},
|
||||||
|
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
|
||||||
|
};
|
||||||
|
|
||||||
|
protected mergeRequestParams(
|
||||||
|
params1: RequestParams,
|
||||||
|
params2?: RequestParams,
|
||||||
|
): RequestParams {
|
||||||
|
return {
|
||||||
|
...this.baseApiParams,
|
||||||
|
...params1,
|
||||||
|
...(params2 || {}),
|
||||||
|
headers: {
|
||||||
|
...(this.baseApiParams.headers || {}),
|
||||||
|
...(params1.headers || {}),
|
||||||
|
...((params2 && params2.headers) || {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createAbortSignal = (
|
||||||
|
cancelToken: CancelToken,
|
||||||
|
): AbortSignal | undefined => {
|
||||||
|
if (this.abortControllers.has(cancelToken)) {
|
||||||
|
const abortController = this.abortControllers.get(cancelToken);
|
||||||
|
if (abortController) {
|
||||||
|
return abortController.signal;
|
||||||
|
}
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
this.abortControllers.set(cancelToken, abortController);
|
||||||
|
return abortController.signal;
|
||||||
|
};
|
||||||
|
|
||||||
|
public abortRequest = (cancelToken: CancelToken) => {
|
||||||
|
const abortController = this.abortControllers.get(cancelToken);
|
||||||
|
|
||||||
|
if (abortController) {
|
||||||
|
abortController.abort();
|
||||||
|
this.abortControllers.delete(cancelToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public request = async <T = any, E = any>({
|
||||||
|
body,
|
||||||
|
secure,
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
query,
|
||||||
|
format,
|
||||||
|
baseUrl,
|
||||||
|
cancelToken,
|
||||||
|
...params
|
||||||
|
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
|
||||||
|
const secureParams =
|
||||||
|
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
|
||||||
|
this.securityWorker &&
|
||||||
|
(await this.securityWorker(this.securityData))) ||
|
||||||
|
{};
|
||||||
|
const requestParams = this.mergeRequestParams(params, secureParams);
|
||||||
|
const queryString = query && this.toQueryString(query);
|
||||||
|
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
|
||||||
|
const responseFormat = format || requestParams.format;
|
||||||
|
|
||||||
|
return this.customFetch(
|
||||||
|
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
|
||||||
|
{
|
||||||
|
...requestParams,
|
||||||
|
headers: {
|
||||||
|
...(requestParams.headers || {}),
|
||||||
|
...(type && type !== ContentType.FormData
|
||||||
|
? { "Content-Type": type }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
signal:
|
||||||
|
(cancelToken
|
||||||
|
? this.createAbortSignal(cancelToken)
|
||||||
|
: requestParams.signal) || null,
|
||||||
|
body:
|
||||||
|
typeof body === "undefined" || body === null
|
||||||
|
? null
|
||||||
|
: payloadFormatter(body),
|
||||||
|
},
|
||||||
|
).then(async (response) => {
|
||||||
|
const r = response as HttpResponse<T, E>;
|
||||||
|
r.data = null as unknown as T;
|
||||||
|
r.error = null as unknown as E;
|
||||||
|
|
||||||
|
const responseToParse = responseFormat ? response.clone() : response;
|
||||||
|
const data = !responseFormat
|
||||||
|
? r
|
||||||
|
: await responseToParse[responseFormat]()
|
||||||
|
.then((data) => {
|
||||||
|
if (r.ok) {
|
||||||
|
r.data = data;
|
||||||
|
} else {
|
||||||
|
r.error = data;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
r.error = e;
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cancelToken) {
|
||||||
|
this.abortControllers.delete(cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) throw data;
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title Chad API
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
export class Api<
|
||||||
|
SecurityDataType extends unknown,
|
||||||
|
> extends HttpClient<SecurityDataType> {
|
||||||
|
chad = {
|
||||||
|
/**
|
||||||
|
* @description Pass file to multipart/form-data
|
||||||
|
*
|
||||||
|
* @tags Attachment
|
||||||
|
* @name AttachmentUpload
|
||||||
|
* @summary Upload attachment
|
||||||
|
* @request POST:/chad/attachment/upload
|
||||||
|
*/
|
||||||
|
attachmentUpload: (params: RequestParams = {}) =>
|
||||||
|
this.request<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/chad/attachment/upload`,
|
||||||
|
method: "POST",
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Attachment
|
||||||
|
* @name AttachmentGet
|
||||||
|
* @summary Get attachment
|
||||||
|
* @request GET:/chad/attachment/{id}
|
||||||
|
*/
|
||||||
|
attachmentGet: (id: string, params: RequestParams = {}) =>
|
||||||
|
this.request<
|
||||||
|
any,
|
||||||
|
{
|
||||||
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/chad/attachment/${id}`,
|
||||||
|
method: "GET",
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Auth
|
||||||
|
* @name AuthRegister
|
||||||
|
* @summary Register
|
||||||
|
* @request POST:/chad/auth/register
|
||||||
|
*/
|
||||||
|
authRegister: (
|
||||||
|
data: {
|
||||||
|
/** @minLength 1 */
|
||||||
|
username: string;
|
||||||
|
/** @minLength 6 */
|
||||||
|
password: string;
|
||||||
|
},
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
|
this.request<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
/** @format date-time */
|
||||||
|
createdAt: string;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/chad/auth/register`,
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Auth
|
||||||
|
* @name AuthLogin
|
||||||
|
* @summary Login
|
||||||
|
* @request POST:/chad/auth/login
|
||||||
|
*/
|
||||||
|
authLogin: (
|
||||||
|
data: {
|
||||||
|
/** @minLength 1 */
|
||||||
|
username: string;
|
||||||
|
/** @minLength 1 */
|
||||||
|
password: string;
|
||||||
|
},
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
|
this.request<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
/** @format date-time */
|
||||||
|
createdAt: string;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/chad/auth/login`,
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Auth
|
||||||
|
* @name AuthMe
|
||||||
|
* @summary Me
|
||||||
|
* @request GET:/chad/auth/me
|
||||||
|
*/
|
||||||
|
authMe: (params: RequestParams = {}) =>
|
||||||
|
this.request<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
/** @format date-time */
|
||||||
|
createdAt: string;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/chad/auth/me`,
|
||||||
|
method: "GET",
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Auth
|
||||||
|
* @name AuthLogout
|
||||||
|
* @summary Logout
|
||||||
|
* @request POST:/chad/auth/logout
|
||||||
|
*/
|
||||||
|
authLogout: (params: RequestParams = {}) =>
|
||||||
|
this.request<
|
||||||
|
any,
|
||||||
|
{
|
||||||
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/chad/auth/logout`,
|
||||||
|
method: "POST",
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Chat
|
||||||
|
* @name ChatSend
|
||||||
|
* @summary Send message
|
||||||
|
* @request POST:/chad/chat/send
|
||||||
|
*/
|
||||||
|
chatSend: (
|
||||||
|
data: {
|
||||||
|
/** @minLength 1 */
|
||||||
|
text: string;
|
||||||
|
},
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
|
this.request<
|
||||||
|
{
|
||||||
|
/** @format uuid */
|
||||||
|
id: string;
|
||||||
|
/** @format uuid */
|
||||||
|
senderId: string;
|
||||||
|
/** @minLength 1 */
|
||||||
|
text: string;
|
||||||
|
/** @format date-time */
|
||||||
|
createdAt: string;
|
||||||
|
/** @format date-time */
|
||||||
|
updatedAt: string;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/chad/chat/send`,
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Chat
|
||||||
|
* @name ChatMessages
|
||||||
|
* @summary Get messages
|
||||||
|
* @request GET:/chad/chat
|
||||||
|
*/
|
||||||
|
chatMessages: (
|
||||||
|
query: {
|
||||||
|
/**
|
||||||
|
* Cursor to message
|
||||||
|
* @format uuid
|
||||||
|
*/
|
||||||
|
cursor?: string;
|
||||||
|
/**
|
||||||
|
* @min 1
|
||||||
|
* @max 100
|
||||||
|
* @default 2
|
||||||
|
*/
|
||||||
|
limit: number;
|
||||||
|
},
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
|
this.request<
|
||||||
|
{
|
||||||
|
messages: {
|
||||||
|
/** @format uuid */
|
||||||
|
id: string;
|
||||||
|
/** @format uuid */
|
||||||
|
senderId: string;
|
||||||
|
/** @minLength 1 */
|
||||||
|
text: string;
|
||||||
|
/** @format date-time */
|
||||||
|
createdAt: string;
|
||||||
|
/** @format date-time */
|
||||||
|
updatedAt: string;
|
||||||
|
}[];
|
||||||
|
/**
|
||||||
|
* Cursor to last message
|
||||||
|
* @format uuid
|
||||||
|
*/
|
||||||
|
nextCursor?: string;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/chad/chat`,
|
||||||
|
method: "GET",
|
||||||
|
query: query,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags User
|
||||||
|
* @name UserGet
|
||||||
|
* @summary Get user
|
||||||
|
* @request GET:/chad/user
|
||||||
|
*/
|
||||||
|
userGet: (
|
||||||
|
query?: {
|
||||||
|
username?: string;
|
||||||
|
},
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
|
this.request<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
/** @format date-time */
|
||||||
|
createdAt: string;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/chad/user`,
|
||||||
|
method: "GET",
|
||||||
|
query: query,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags User
|
||||||
|
* @name UserGetPreferences
|
||||||
|
* @summary Get preferences
|
||||||
|
* @request GET:/chad/user/preferences
|
||||||
|
*/
|
||||||
|
userGetPreferences: (params: RequestParams = {}) =>
|
||||||
|
this.request<
|
||||||
|
{
|
||||||
|
toggleInputHotkey: string;
|
||||||
|
toggleOutputHotkey: string;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/chad/user/preferences`,
|
||||||
|
method: "GET",
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags User
|
||||||
|
* @name UserUpdatePreferences
|
||||||
|
* @summary Update preferences
|
||||||
|
* @request PATCH:/chad/user/preferences
|
||||||
|
*/
|
||||||
|
userUpdatePreferences: (
|
||||||
|
data: {
|
||||||
|
toggleInputHotkey?: string;
|
||||||
|
toggleOutputHotkey?: string;
|
||||||
|
},
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
|
this.request<
|
||||||
|
any,
|
||||||
|
{
|
||||||
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/chad/user/preferences`,
|
||||||
|
method: "PATCH",
|
||||||
|
body: data,
|
||||||
|
type: ContentType.Json,
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags User
|
||||||
|
* @name UserUpdateProfile
|
||||||
|
* @summary Update profile
|
||||||
|
* @request PATCH:/chad/profile
|
||||||
|
*/
|
||||||
|
userUpdateProfile: (
|
||||||
|
data: {
|
||||||
|
displayName: string;
|
||||||
|
},
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
|
this.request<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
/** @format date-time */
|
||||||
|
createdAt: string;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/chad/profile`,
|
||||||
|
method: "PATCH",
|
||||||
|
body: data,
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { PrismaAdapter } from '@lucia-auth/adapter-prisma'
|
|
||||||
import { Lucia } from 'lucia'
|
|
||||||
import prisma from '../prisma/client.ts'
|
|
||||||
|
|
||||||
declare module 'lucia' {
|
|
||||||
interface Register {
|
|
||||||
Lucia: typeof Lucia
|
|
||||||
UserId: string
|
|
||||||
DatabaseUserAttributes: DatabaseUserAttributes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DatabaseUserAttributes {
|
|
||||||
id: string
|
|
||||||
displayName: string
|
|
||||||
username: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const auth = new Lucia(new PrismaAdapter(prisma.session, prisma.user), {
|
|
||||||
sessionCookie: {
|
|
||||||
attributes: {
|
|
||||||
sameSite: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
getUserAttributes: ({ id, displayName, username }) => {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
displayName,
|
|
||||||
username,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export type Auth = typeof auth
|
|
||||||
6
server/nodemon.json
Normal file
6
server/nodemon.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"watch": ["."],
|
||||||
|
"ext": ".ts,.js",
|
||||||
|
"ignore": ["node_modules", ".idea", "dist"],
|
||||||
|
"exec": "ts-node --transpile-only server.ts"
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "nodemon",
|
||||||
"start": "ts-node --transpile-only server.ts",
|
"start": "ts-node --transpile-only server.ts",
|
||||||
"db:deploy": "npx prisma migrate deploy && npx prisma generate"
|
"db:deploy": "npx prisma migrate deploy && npx prisma generate"
|
||||||
},
|
},
|
||||||
@@ -11,8 +12,13 @@
|
|||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/cors": "^11.1.0",
|
"@fastify/cors": "^11.1.0",
|
||||||
"@fastify/multipart": "^10.0.0",
|
"@fastify/multipart": "^10.0.0",
|
||||||
|
"@fastify/sensible": "^6.0.4",
|
||||||
|
"@fastify/swagger": "^9.7.0",
|
||||||
|
"@fastify/type-provider-typebox": "^6.1.0",
|
||||||
"@lucia-auth/adapter-prisma": "^4.0.1",
|
"@lucia-auth/adapter-prisma": "^4.0.1",
|
||||||
"@prisma/client": "^6.17.0",
|
"@prisma/adapter-better-sqlite3": "^7.7.0",
|
||||||
|
"@prisma/client": "7",
|
||||||
|
"@scalar/fastify-api-reference": "^1.52.3",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
@@ -20,8 +26,9 @@
|
|||||||
"fastify-plugin": "^5.1.0",
|
"fastify-plugin": "^5.1.0",
|
||||||
"lucia": "^3.2.2",
|
"lucia": "^3.2.2",
|
||||||
"mediasoup": "^3.19.3",
|
"mediasoup": "^3.19.3",
|
||||||
"prisma": "^6.17.0",
|
"prisma": "7",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
|
"typebox": "^1.1.27",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
@@ -29,8 +36,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^5.4.1",
|
"@antfu/eslint-config": "^5.4.1",
|
||||||
"@types/bcrypt": "^6",
|
"@types/bcrypt": "^6",
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/ws": "^8",
|
"@types/ws": "^8",
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.36.0",
|
||||||
|
"nodemon": "^3.1.14",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,40 +1,76 @@
|
|||||||
import type { Session, User } from 'lucia'
|
import type { Session } from 'lucia'
|
||||||
|
import { PrismaAdapter } from '@lucia-auth/adapter-prisma'
|
||||||
import fp from 'fastify-plugin'
|
import fp from 'fastify-plugin'
|
||||||
import { auth } from '../auth/lucia.ts'
|
import { Lucia } from 'lucia'
|
||||||
|
|
||||||
|
interface DatabaseUserAttributes {
|
||||||
|
id: string
|
||||||
|
displayName: string
|
||||||
|
username: string
|
||||||
|
createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'lucia' {
|
||||||
|
interface Register {
|
||||||
|
Lucia: Lucia
|
||||||
|
UserId: string
|
||||||
|
DatabaseUserAttributes: DatabaseUserAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
declare module 'fastify' {
|
declare module 'fastify' {
|
||||||
|
interface FastifyInstance {
|
||||||
|
lucia: Lucia
|
||||||
|
}
|
||||||
|
|
||||||
interface FastifyRequest {
|
interface FastifyRequest {
|
||||||
user: User | null
|
user: DatabaseUserAttributes | null
|
||||||
session: Session | null
|
session: Session | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FastifyContextConfig {
|
||||||
|
skipAuth: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default fp(async (fastify) => {
|
export default fp(async (fastify) => {
|
||||||
|
const lucia = new Lucia<any, DatabaseUserAttributes>(new PrismaAdapter(fastify.prisma.session, fastify.prisma.user), {
|
||||||
|
sessionCookie: {
|
||||||
|
attributes: {
|
||||||
|
sameSite: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getUserAttributes: (attrs) => {
|
||||||
|
return attrs
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
fastify.decorate('lucia', lucia)
|
||||||
fastify.decorateRequest('user', null)
|
fastify.decorateRequest('user', null)
|
||||||
fastify.decorateRequest('session', null)
|
fastify.decorateRequest('session', null)
|
||||||
|
|
||||||
fastify.addHook('preHandler', async (req, reply) => {
|
fastify.addHook('onRequest', async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const sessionId = auth.readSessionCookie(req.headers.cookie ?? '')
|
const sessionId = lucia.readSessionCookie(req.headers.cookie ?? '')
|
||||||
|
|
||||||
if (!sessionId)
|
if (!sessionId)
|
||||||
return
|
return
|
||||||
|
|
||||||
const { session, user } = await auth.validateSession(sessionId ?? '')
|
const { session, user } = await lucia.validateSession(sessionId ?? '')
|
||||||
|
|
||||||
if (session && session.fresh) {
|
if (session && session.fresh) {
|
||||||
const cookie = auth.createSessionCookie(session.id)
|
const cookie = lucia.createSessionCookie(session.id)
|
||||||
|
|
||||||
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
|
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const blank = auth.createBlankSessionCookie()
|
const blank = lucia.createBlankSessionCookie()
|
||||||
|
|
||||||
reply.setCookie(blank.name, blank.value, blank.attributes)
|
reply.setCookie(blank.name, blank.value, blank.attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = user
|
req.user = user as DatabaseUserAttributes
|
||||||
req.session = session
|
req.session = session
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
@@ -42,4 +78,21 @@ export default fp(async (fastify) => {
|
|||||||
req.session = null
|
req.session = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fastify.addHook('onRequest', (req, reply, done) => {
|
||||||
|
if (req.is404 || req.routeOptions.schema?.hide || req.routeOptions.config.skipAuth) {
|
||||||
|
done()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.user) {
|
||||||
|
reply.unauthorized()
|
||||||
|
}
|
||||||
|
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
name: 'auth',
|
||||||
|
dependencies: ['prisma'],
|
||||||
})
|
})
|
||||||
|
|||||||
27
server/plugins/event-bus.ts
Normal file
27
server/plugins/event-bus.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { FastifyPluginAsync } from 'fastify'
|
||||||
|
import type { Type } from 'typebox'
|
||||||
|
import type { UserSchema } from '../schemas/auth.ts'
|
||||||
|
import type { ChatMessageSchema } from '../schemas/chat.ts'
|
||||||
|
import { EventEmitter } from 'node:events'
|
||||||
|
import fp from 'fastify-plugin'
|
||||||
|
|
||||||
|
declare module 'fastify' {
|
||||||
|
interface FastifyInstance {
|
||||||
|
bus: EventEmitter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventMap {
|
||||||
|
'chat:new-message': [Type.Static<typeof ChatMessageSchema>]
|
||||||
|
'user:profile-updated': [Type.Static<typeof UserSchema>]
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin: FastifyPluginAsync = fp(async (fastify) => {
|
||||||
|
const bus = new EventEmitter<EventMap>()
|
||||||
|
|
||||||
|
fastify.decorate('bus', bus)
|
||||||
|
}, {
|
||||||
|
name: 'event-bus',
|
||||||
|
})
|
||||||
|
|
||||||
|
export default plugin
|
||||||
32
server/plugins/prisma.ts
Normal file
32
server/plugins/prisma.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { FastifyPluginAsync } from 'fastify'
|
||||||
|
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
|
||||||
|
import fp from 'fastify-plugin'
|
||||||
|
import { PrismaClient } from '../prisma/generated-client/client.ts'
|
||||||
|
|
||||||
|
declare module 'fastify' {
|
||||||
|
interface FastifyInstance {
|
||||||
|
prisma: PrismaClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin: FastifyPluginAsync = fp(async (fastify) => {
|
||||||
|
const prisma = new PrismaClient({
|
||||||
|
log: ['query', 'error', 'warn'],
|
||||||
|
adapter: new PrismaBetterSqlite3({
|
||||||
|
url: process.env.DATABASE_URL!,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.$connect()
|
||||||
|
|
||||||
|
fastify.log.info('Testing DB Connection. OK')
|
||||||
|
|
||||||
|
fastify.decorate('prisma', prisma)
|
||||||
|
fastify.addHook('onClose', async (fastify) => {
|
||||||
|
await fastify.prisma.$disconnect()
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
name: 'prisma',
|
||||||
|
})
|
||||||
|
|
||||||
|
export default plugin
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { FastifyInstance } from 'fastify'
|
import type { FastifyInstance } from 'fastify'
|
||||||
import type { ServerOptions } from 'socket.io'
|
import type { ServerOptions } from 'socket.io'
|
||||||
|
import type { MessageSelect } from '../prisma/generated-client/models/Message.ts'
|
||||||
import fp from 'fastify-plugin'
|
import fp from 'fastify-plugin'
|
||||||
import { Server } from 'socket.io'
|
import { Server } from 'socket.io'
|
||||||
import registerChatSocket from '../socket/chat.ts'
|
import registerChatSocket from '../socket/chat.ts'
|
||||||
@@ -23,9 +24,11 @@ export default fp<Partial<ServerOptions>>(
|
|||||||
await fastify.io.close()
|
await fastify.io.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.ready(async () => {
|
await registerWebrtcSocket(fastify.io, fastify.mediasoupRouter, fastify.prisma)
|
||||||
await registerWebrtcSocket(fastify.io, fastify.mediasoupRouter)
|
|
||||||
await registerChatSocket(fastify.io)
|
await registerChatSocket(fastify.io)
|
||||||
|
|
||||||
|
fastify.bus.on('chat:new-message', async (message: MessageSelect) => {
|
||||||
|
fastify.io.emit('chat:new-message', message)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ name: 'socket-io', dependencies: ['mediasoup-worker', 'mediasoup-router'] },
|
{ name: 'socket-io', dependencies: ['mediasoup-worker', 'mediasoup-router'] },
|
||||||
|
|||||||
13
server/prisma.config.ts
Normal file
13
server/prisma.config.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig, env } from 'prisma/config'
|
||||||
|
import 'dotenv/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: './prisma/schema.prisma',
|
||||||
|
migrations: {
|
||||||
|
path: './prisma/migrations',
|
||||||
|
seed: 'ts-node ./prisma/seed.ts',
|
||||||
|
},
|
||||||
|
datasource: {
|
||||||
|
url: env('DATABASE_URL'),
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client'
|
|
||||||
|
|
||||||
const client = new PrismaClient({
|
|
||||||
log: ['query', 'error', 'warn'],
|
|
||||||
})
|
|
||||||
|
|
||||||
export default client
|
|
||||||
54
server/prisma/generated-client/browser.ts
Normal file
54
server/prisma/generated-client/browser.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||||
|
* Use it to get access to models, enums, and input types.
|
||||||
|
*
|
||||||
|
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
|
||||||
|
* See `client.ts` for the standard, server-side entry point.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Prisma from './internal/prismaNamespaceBrowser.ts'
|
||||||
|
export { Prisma }
|
||||||
|
export * as $Enums from './enums.ts'
|
||||||
|
export * from './enums.ts';
|
||||||
|
/**
|
||||||
|
* Model User
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type User = Prisma.UserModel
|
||||||
|
/**
|
||||||
|
* Model Session
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Session = Prisma.SessionModel
|
||||||
|
/**
|
||||||
|
* Model UserPreferences
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type UserPreferences = Prisma.UserPreferencesModel
|
||||||
|
/**
|
||||||
|
* Model Attachment
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Attachment = Prisma.AttachmentModel
|
||||||
|
/**
|
||||||
|
* Model Message
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Message = Prisma.MessageModel
|
||||||
|
/**
|
||||||
|
* Model MessageAttachment
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type MessageAttachment = Prisma.MessageAttachmentModel
|
||||||
|
/**
|
||||||
|
* Model Channel
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Channel = Prisma.ChannelModel
|
||||||
78
server/prisma/generated-client/client.ts
Normal file
78
server/prisma/generated-client/client.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
|
||||||
|
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as process from 'node:process'
|
||||||
|
import * as path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
import * as runtime from "@prisma/client/runtime/client"
|
||||||
|
import * as $Enums from "./enums.ts"
|
||||||
|
import * as $Class from "./internal/class.ts"
|
||||||
|
import * as Prisma from "./internal/prismaNamespace.ts"
|
||||||
|
|
||||||
|
export * as $Enums from './enums.ts'
|
||||||
|
export * from "./enums.ts"
|
||||||
|
/**
|
||||||
|
* ## Prisma Client
|
||||||
|
*
|
||||||
|
* Type-safe database client for TypeScript
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const prisma = new PrismaClient({
|
||||||
|
* adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL })
|
||||||
|
* })
|
||||||
|
* // Fetch zero or more Users
|
||||||
|
* const users = await prisma.user.findMany()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/client).
|
||||||
|
*/
|
||||||
|
export const PrismaClient = $Class.getPrismaClientClass()
|
||||||
|
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||||
|
export { Prisma }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model User
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type User = Prisma.UserModel
|
||||||
|
/**
|
||||||
|
* Model Session
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Session = Prisma.SessionModel
|
||||||
|
/**
|
||||||
|
* Model UserPreferences
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type UserPreferences = Prisma.UserPreferencesModel
|
||||||
|
/**
|
||||||
|
* Model Attachment
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Attachment = Prisma.AttachmentModel
|
||||||
|
/**
|
||||||
|
* Model Message
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Message = Prisma.MessageModel
|
||||||
|
/**
|
||||||
|
* Model MessageAttachment
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type MessageAttachment = Prisma.MessageAttachmentModel
|
||||||
|
/**
|
||||||
|
* Model Channel
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Channel = Prisma.ChannelModel
|
||||||
298
server/prisma/generated-client/commonInputTypes.ts
Normal file
298
server/prisma/generated-client/commonInputTypes.ts
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as runtime from "@prisma/client/runtime/client"
|
||||||
|
import * as $Enums from "./enums.ts"
|
||||||
|
import type * as Prisma from "./internal/prismaNamespace.ts"
|
||||||
|
|
||||||
|
|
||||||
|
export type StringFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[]
|
||||||
|
notIn?: string[]
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[]
|
||||||
|
notIn?: Date[] | string[]
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[]
|
||||||
|
notIn?: string[]
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[]
|
||||||
|
notIn?: Date[] | string[]
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | null
|
||||||
|
notIn?: string[] | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SortOrderInput = {
|
||||||
|
sort: Prisma.SortOrder
|
||||||
|
nulls?: Prisma.NullsOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | null
|
||||||
|
notIn?: string[] | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IntFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[]
|
||||||
|
notIn?: number[]
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[]
|
||||||
|
notIn?: number[]
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoolFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[]
|
||||||
|
notIn?: string[]
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[]
|
||||||
|
notIn?: Date[] | string[]
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[]
|
||||||
|
notIn?: string[]
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedIntFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[]
|
||||||
|
notIn?: number[]
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[]
|
||||||
|
notIn?: Date[] | string[]
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | null
|
||||||
|
notIn?: string[] | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | null
|
||||||
|
notIn?: string[] | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: number[] | null
|
||||||
|
notIn?: number[] | null
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[]
|
||||||
|
notIn?: number[]
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[]
|
||||||
|
notIn?: number[]
|
||||||
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedBoolFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
15
server/prisma/generated-client/enums.ts
Normal file
15
server/prisma/generated-client/enums.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file exports all enum related types from the schema.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// This file is empty because there are no enums in the schema.
|
||||||
|
export {}
|
||||||
264
server/prisma/generated-client/internal/class.ts
Normal file
264
server/prisma/generated-client/internal/class.ts
Normal file
File diff suppressed because one or more lines are too long
1256
server/prisma/generated-client/internal/prismaNamespace.ts
Normal file
1256
server/prisma/generated-client/internal/prismaNamespace.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,159 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* WARNING: This is an internal file that is subject to change!
|
||||||
|
*
|
||||||
|
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||||
|
*
|
||||||
|
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
|
||||||
|
* While this enables partial backward compatibility, it is not part of the stable public API.
|
||||||
|
*
|
||||||
|
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
|
||||||
|
* model files in the `model` directory!
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as runtime from "@prisma/client/runtime/index-browser"
|
||||||
|
|
||||||
|
export type * from '../models.ts'
|
||||||
|
export type * from './prismaNamespace.ts'
|
||||||
|
|
||||||
|
export const Decimal = runtime.Decimal
|
||||||
|
|
||||||
|
|
||||||
|
export const NullTypes = {
|
||||||
|
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||||
|
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||||
|
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const DbNull = runtime.DbNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const JsonNull = runtime.JsonNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const AnyNull = runtime.AnyNull
|
||||||
|
|
||||||
|
|
||||||
|
export const ModelName = {
|
||||||
|
User: 'User',
|
||||||
|
Session: 'Session',
|
||||||
|
UserPreferences: 'UserPreferences',
|
||||||
|
Attachment: 'Attachment',
|
||||||
|
Message: 'Message',
|
||||||
|
MessageAttachment: 'MessageAttachment',
|
||||||
|
Channel: 'Channel'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const TransactionIsolationLevel = runtime.makeStrictEnum({
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
} as const)
|
||||||
|
|
||||||
|
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||||
|
|
||||||
|
|
||||||
|
export const UserScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
username: 'username',
|
||||||
|
password: 'password',
|
||||||
|
displayName: 'displayName',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const SessionScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
expiresAt: 'expiresAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type SessionScalarFieldEnum = (typeof SessionScalarFieldEnum)[keyof typeof SessionScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const UserPreferencesScalarFieldEnum = {
|
||||||
|
userId: 'userId',
|
||||||
|
toggleInputHotkey: 'toggleInputHotkey',
|
||||||
|
toggleOutputHotkey: 'toggleOutputHotkey'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type UserPreferencesScalarFieldEnum = (typeof UserPreferencesScalarFieldEnum)[keyof typeof UserPreferencesScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const AttachmentScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
name: 'name',
|
||||||
|
mimetype: 'mimetype',
|
||||||
|
size: 'size',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type AttachmentScalarFieldEnum = (typeof AttachmentScalarFieldEnum)[keyof typeof AttachmentScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const MessageScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
text: 'text',
|
||||||
|
senderId: 'senderId',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type MessageScalarFieldEnum = (typeof MessageScalarFieldEnum)[keyof typeof MessageScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const MessageAttachmentScalarFieldEnum = {
|
||||||
|
messageId: 'messageId',
|
||||||
|
attachmentId: 'attachmentId'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type MessageAttachmentScalarFieldEnum = (typeof MessageAttachmentScalarFieldEnum)[keyof typeof MessageAttachmentScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ChannelScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
name: 'name',
|
||||||
|
persistent: 'persistent'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ChannelScalarFieldEnum = (typeof ChannelScalarFieldEnum)[keyof typeof ChannelScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const SortOrder = {
|
||||||
|
asc: 'asc',
|
||||||
|
desc: 'desc'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||||
|
|
||||||
|
|
||||||
|
export const NullsOrder = {
|
||||||
|
first: 'first',
|
||||||
|
last: 'last'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||||
|
|
||||||
18
server/prisma/generated-client/models.ts
Normal file
18
server/prisma/generated-client/models.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This is a barrel export file for all models and their related types.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
export type * from './models/User.ts'
|
||||||
|
export type * from './models/Session.ts'
|
||||||
|
export type * from './models/UserPreferences.ts'
|
||||||
|
export type * from './models/Attachment.ts'
|
||||||
|
export type * from './models/Message.ts'
|
||||||
|
export type * from './models/MessageAttachment.ts'
|
||||||
|
export type * from './models/Channel.ts'
|
||||||
|
export type * from './commonInputTypes.ts'
|
||||||
1377
server/prisma/generated-client/models/Attachment.ts
Normal file
1377
server/prisma/generated-client/models/Attachment.ts
Normal file
File diff suppressed because it is too large
Load Diff
1095
server/prisma/generated-client/models/Channel.ts
Normal file
1095
server/prisma/generated-client/models/Channel.ts
Normal file
File diff suppressed because it is too large
Load Diff
1498
server/prisma/generated-client/models/Message.ts
Normal file
1498
server/prisma/generated-client/models/Message.ts
Normal file
File diff suppressed because it is too large
Load Diff
1345
server/prisma/generated-client/models/MessageAttachment.ts
Normal file
1345
server/prisma/generated-client/models/MessageAttachment.ts
Normal file
File diff suppressed because it is too large
Load Diff
1272
server/prisma/generated-client/models/Session.ts
Normal file
1272
server/prisma/generated-client/models/Session.ts
Normal file
File diff suppressed because it is too large
Load Diff
1602
server/prisma/generated-client/models/User.ts
Normal file
1602
server/prisma/generated-client/models/User.ts
Normal file
File diff suppressed because it is too large
Load Diff
1233
server/prisma/generated-client/models/UserPreferences.ts
Normal file
1233
server/prisma/generated-client/models/UserPreferences.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `volumes` on the `UserPreferences` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Attachment" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"mimetype" TEXT NOT NULL,
|
||||||
|
"size" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_UserPreferences" (
|
||||||
|
"userId" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"toggleInputHotkey" TEXT DEFAULT '',
|
||||||
|
"toggleOutputHotkey" TEXT DEFAULT '',
|
||||||
|
CONSTRAINT "UserPreferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_UserPreferences" ("toggleInputHotkey", "toggleOutputHotkey", "userId") SELECT "toggleInputHotkey", "toggleOutputHotkey", "userId" FROM "UserPreferences";
|
||||||
|
DROP TABLE "UserPreferences";
|
||||||
|
ALTER TABLE "new_UserPreferences" RENAME TO "UserPreferences";
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Attachment" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"mimetype" TEXT NOT NULL,
|
||||||
|
"size" INTEGER NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Attachment" ("id", "mimetype", "name", "size") SELECT "id", "mimetype", "name", "size" FROM "Attachment";
|
||||||
|
DROP TABLE "Attachment";
|
||||||
|
ALTER TABLE "new_Attachment" RENAME TO "Attachment";
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_User" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"displayName" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "User_id_fkey" FOREIGN KEY ("id") REFERENCES "UserPreferences" ("userId") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_User" ("createdAt", "displayName", "id", "password", "updatedAt", "username") SELECT "createdAt", "displayName", "id", "password", "updatedAt", "username" FROM "User";
|
||||||
|
DROP TABLE "User";
|
||||||
|
ALTER TABLE "new_User" RENAME TO "User";
|
||||||
|
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||||
|
CREATE TABLE "new_UserPreferences" (
|
||||||
|
"userId" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"toggleInputHotkey" TEXT DEFAULT '',
|
||||||
|
"toggleOutputHotkey" TEXT DEFAULT ''
|
||||||
|
);
|
||||||
|
INSERT INTO "new_UserPreferences" ("toggleInputHotkey", "toggleOutputHotkey", "userId") SELECT "toggleInputHotkey", "toggleOutputHotkey", "userId" FROM "UserPreferences";
|
||||||
|
DROP TABLE "UserPreferences";
|
||||||
|
ALTER TABLE "new_UserPreferences" RENAME TO "UserPreferences";
|
||||||
|
CREATE UNIQUE INDEX "UserPreferences_userId_key" ON "UserPreferences"("userId");
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_User" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"displayName" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO "new_User" ("createdAt", "displayName", "id", "password", "updatedAt", "username") SELECT "createdAt", "displayName", "id", "password", "updatedAt", "username" FROM "User";
|
||||||
|
DROP TABLE "User";
|
||||||
|
ALTER TABLE "new_User" RENAME TO "User";
|
||||||
|
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||||
|
CREATE TABLE "new_UserPreferences" (
|
||||||
|
"userId" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"toggleInputHotkey" TEXT DEFAULT '',
|
||||||
|
"toggleOutputHotkey" TEXT DEFAULT '',
|
||||||
|
CONSTRAINT "UserPreferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_UserPreferences" ("toggleInputHotkey", "toggleOutputHotkey", "userId") SELECT "toggleInputHotkey", "toggleOutputHotkey", "userId" FROM "UserPreferences";
|
||||||
|
DROP TABLE "UserPreferences";
|
||||||
|
ALTER TABLE "new_UserPreferences" RENAME TO "UserPreferences";
|
||||||
|
CREATE UNIQUE INDEX "UserPreferences_userId_key" ON "UserPreferences"("userId");
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Message" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"senderId" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "Message_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "MessageAttachment" (
|
||||||
|
"messageId" TEXT NOT NULL,
|
||||||
|
"attachmentId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY ("messageId", "attachmentId"),
|
||||||
|
CONSTRAINT "MessageAttachment_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "MessageAttachment_attachmentId_fkey" FOREIGN KEY ("attachmentId") REFERENCES "Attachment" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Channel" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"persistent" BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
url = env("DATABASE_URL")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client"
|
||||||
// output = "./generated/client"
|
output = "./generated-client"
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
@@ -18,6 +17,7 @@ model User {
|
|||||||
|
|
||||||
Session Session[]
|
Session Session[]
|
||||||
UserPreferences UserPreferences?
|
UserPreferences UserPreferences?
|
||||||
|
Messages Message[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
@@ -31,10 +31,46 @@ model Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model UserPreferences {
|
model UserPreferences {
|
||||||
userId String @id
|
userId String @id @unique
|
||||||
toggleInputHotkey String? @default("")
|
toggleInputHotkey String? @default("")
|
||||||
toggleOutputHotkey String? @default("")
|
toggleOutputHotkey String? @default("")
|
||||||
volumes Json? @default("{}")
|
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(references: [id], fields: [userId], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model Attachment {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String
|
||||||
|
mimetype String
|
||||||
|
size Int
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
message MessageAttachment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Message {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
text String
|
||||||
|
senderId String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
sender User? @relation(references: [id], fields: [senderId], onDelete: SetNull)
|
||||||
|
attachments MessageAttachment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model MessageAttachment {
|
||||||
|
messageId String
|
||||||
|
attachmentId String
|
||||||
|
|
||||||
|
message Message @relation(fields: [messageId], references: [id])
|
||||||
|
attachment Attachment @relation(fields: [attachmentId], references: [id])
|
||||||
|
|
||||||
|
@@id([messageId, attachmentId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Channel {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String
|
||||||
|
persistent Boolean
|
||||||
}
|
}
|
||||||
33
server/prisma/seed.ts
Normal file
33
server/prisma/seed.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
|
||||||
|
import { PrismaClient } from './generated-client/client.ts'
|
||||||
|
import 'dotenv/config'
|
||||||
|
|
||||||
|
const prisma = new PrismaClient({
|
||||||
|
adapter: new PrismaBetterSqlite3({
|
||||||
|
url: process.env.DATABASE_URL!,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
async function main() {
|
||||||
|
const _now = new Date()
|
||||||
|
|
||||||
|
await prisma.channel.upsert({
|
||||||
|
where: { id: 'default' },
|
||||||
|
create: {
|
||||||
|
id: 'default',
|
||||||
|
name: 'Default channel',
|
||||||
|
persistent: true,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
persistent: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
main()
|
||||||
|
.then(async () => {
|
||||||
|
await prisma.$disconnect()
|
||||||
|
})
|
||||||
|
.catch(async (e) => {
|
||||||
|
console.error(e)
|
||||||
|
await prisma.$disconnect()
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
96
server/routes/attachment.ts
Normal file
96
server/routes/attachment.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
|
||||||
|
import * as fs from 'node:fs'
|
||||||
|
import * as path from 'node:path'
|
||||||
|
import { Type } from 'typebox'
|
||||||
|
|
||||||
|
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||||
|
const uploadDir = path.join(process.cwd(), 'uploads')
|
||||||
|
|
||||||
|
if (!fs.existsSync(uploadDir)) {
|
||||||
|
fs.mkdirSync(uploadDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
fastify.post(
|
||||||
|
'/attachment/upload',
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
summary: 'Upload attachment',
|
||||||
|
tags: ['Attachment'],
|
||||||
|
operationId: 'attachment.upload',
|
||||||
|
description: 'Pass file to multipart/form-data',
|
||||||
|
response: {
|
||||||
|
200: Type.String({ format: 'uuid', description: 'Attachment UUID' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (req, reply) => {
|
||||||
|
const data = await req.file()
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return reply.notAcceptable()
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = await fastify.prisma.attachment.create({
|
||||||
|
data: {
|
||||||
|
name: data.filename,
|
||||||
|
mimetype: data.mimetype,
|
||||||
|
size: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!meta) {
|
||||||
|
return reply.notAcceptable()
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(process.cwd(), 'uploads', meta.id)
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const writeStream = fs.createWriteStream(filePath)
|
||||||
|
data.file.pipe(writeStream)
|
||||||
|
data.file.on('end', resolve)
|
||||||
|
data.file.on('error', reject)
|
||||||
|
})
|
||||||
|
|
||||||
|
return meta.id
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fastify.get(
|
||||||
|
'/attachment/:id',
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
summary: 'Get attachment',
|
||||||
|
tags: ['Attachment'],
|
||||||
|
operationId: 'attachment.get',
|
||||||
|
params: Type.Object({
|
||||||
|
id: Type.String({ format: 'uuid' }),
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: Type.Any({ description: 'Attachment content' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
skipAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (req, reply) => {
|
||||||
|
const meta = await fastify.prisma.attachment.findFirst({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!meta) {
|
||||||
|
return reply.notFound('Attachment not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(process.cwd(), 'uploads', meta.id)
|
||||||
|
|
||||||
|
reply.type(meta.mimetype)
|
||||||
|
reply.header('Cache-Control', 'public, max-age=31536000')
|
||||||
|
reply.header('Content-Disposition', `inline; filename="${meta.name}"`)
|
||||||
|
|
||||||
|
return fs.createReadStream(filePath)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default plugin
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import type { FastifyInstance } from 'fastify'
|
|
||||||
import bcrypt from 'bcrypt'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
export default function (fastify: FastifyInstance) {
|
|
||||||
fastify.post('/attachments/upload', async (req, reply) => {
|
|
||||||
try {
|
|
||||||
const schema = z.object({
|
|
||||||
file: z.file(),
|
|
||||||
})
|
|
||||||
const input = schema.parse(req.body)
|
|
||||||
|
|
||||||
// const file = req.file({ limits: { } })
|
|
||||||
|
|
||||||
const id = await bcrypt.hash(input.file, 10)
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
fastify.log.error(err)
|
|
||||||
reply.code(400)
|
|
||||||
|
|
||||||
if (err instanceof z.ZodError) {
|
|
||||||
reply.send({ error: z.prettifyError(err) })
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
reply.send({ error: err.message })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,118 +1,146 @@
|
|||||||
import type { FastifyInstance } from 'fastify'
|
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
|
||||||
import bcrypt from 'bcrypt'
|
import bcrypt from 'bcrypt'
|
||||||
import { z } from 'zod'
|
import { Type } from 'typebox'
|
||||||
import { auth } from '../auth/lucia.ts'
|
import { CreateUserSchema, UserSchema } from '../schemas/auth.ts'
|
||||||
import prisma from '../prisma/client.ts'
|
|
||||||
|
|
||||||
export default function (fastify: FastifyInstance) {
|
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||||
fastify.post('/register', async (req, reply) => {
|
fastify.post(
|
||||||
try {
|
'/auth/register',
|
||||||
const schema = z.object({
|
{
|
||||||
username: z.string().min(1),
|
schema: {
|
||||||
password: z.string().min(6),
|
summary: 'Register',
|
||||||
})
|
tags: ['Auth'],
|
||||||
const input = schema.parse(req.body)
|
operationId: 'auth.register',
|
||||||
|
body: CreateUserSchema,
|
||||||
const hashed = await bcrypt.hash(input.password, 10)
|
response: {
|
||||||
const user = await prisma.user.create({
|
200: UserSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
skipAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (req, reply) => {
|
||||||
|
const hashed = await bcrypt.hash(req.body.password, 10)
|
||||||
|
const user = await fastify.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
username: input.username,
|
username: req.body.username,
|
||||||
password: hashed,
|
password: hashed,
|
||||||
displayName: input.username,
|
displayName: req.body.username,
|
||||||
|
UserPreferences: {
|
||||||
|
create: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const session = await auth.createSession(user.id, {})
|
const session = await fastify.lucia.createSession(user.id, {})
|
||||||
const cookie = auth.createSessionCookie(session.id)
|
const cookie = fastify.lucia.createSessionCookie(session.id)
|
||||||
|
|
||||||
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
|
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName,
|
displayName: user.username,
|
||||||
|
createdAt: user.createdAt.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
catch (err) {
|
)
|
||||||
fastify.log.error(err)
|
|
||||||
reply.code(400)
|
|
||||||
|
|
||||||
if (err instanceof z.ZodError) {
|
fastify.post(
|
||||||
reply.send({ error: z.prettifyError(err) })
|
'/auth/login',
|
||||||
}
|
{
|
||||||
else {
|
schema: {
|
||||||
reply.send({ error: err.message })
|
summary: 'Login',
|
||||||
}
|
tags: ['Auth'],
|
||||||
}
|
operationId: 'auth.login',
|
||||||
})
|
body: Type.Object({
|
||||||
|
username: Type.String({ minLength: 1 }),
|
||||||
fastify.post('/login', async (req, reply) => {
|
password: Type.String({ minLength: 1 }),
|
||||||
try {
|
}),
|
||||||
const schema = z.object({
|
response: {
|
||||||
username: z.string().min(1),
|
200: UserSchema,
|
||||||
password: z.string(),
|
},
|
||||||
})
|
},
|
||||||
const input = schema.parse(req.body)
|
config: {
|
||||||
|
skipAuth: true,
|
||||||
const user = await prisma.user.findFirst({
|
},
|
||||||
where: { username: input.username },
|
},
|
||||||
|
async (req, reply) => {
|
||||||
|
const user = await fastify.prisma.user.findFirst({
|
||||||
|
where: { username: req.body.username },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
displayName: true,
|
||||||
|
createdAt: true,
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return reply.code(404).send({ error: 'Incorrect username or password' })
|
return reply.notFound('Incorrect username or password')
|
||||||
}
|
}
|
||||||
|
|
||||||
const validPassword = await bcrypt.compare(input.password, user.password)
|
const validPassword = await bcrypt.compare(req.body.password, user.password)
|
||||||
if (!validPassword) {
|
if (!validPassword) {
|
||||||
return reply.code(404).send({ error: 'Incorrect username or password' })
|
return reply.notFound('Incorrect username or password')
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await auth.createSession(user.id, {})
|
const session = await fastify.lucia.createSession(user.id, {})
|
||||||
const cookie = auth.createSessionCookie(session.id)
|
const cookie = fastify.lucia.createSessionCookie(session.id)
|
||||||
|
|
||||||
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
|
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
createdAt: user.createdAt.toISOString(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fastify.get(
|
||||||
|
'/auth/me',
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
summary: 'Me',
|
||||||
|
tags: ['Auth'],
|
||||||
|
operationId: 'auth.me',
|
||||||
|
response: {
|
||||||
|
200: UserSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (req) => {
|
||||||
|
const user = req.user!
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
|
createdAt: user.createdAt.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
catch (err) {
|
)
|
||||||
fastify.log.error(err)
|
|
||||||
reply.code(400)
|
|
||||||
|
|
||||||
if (err instanceof z.ZodError) {
|
fastify.post(
|
||||||
reply.send({ error: z.prettifyError(err) })
|
'/auth/logout',
|
||||||
}
|
{
|
||||||
else {
|
schema: {
|
||||||
reply.send({ error: err.message })
|
summary: 'Logout',
|
||||||
}
|
tags: ['Auth'],
|
||||||
}
|
operationId: 'auth.logout',
|
||||||
})
|
},
|
||||||
|
},
|
||||||
fastify.get('/me', async (req, reply) => {
|
async (req, reply) => {
|
||||||
if (req.user) {
|
|
||||||
return req.user
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.code(401).send(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
fastify.post('/logout', async (req, reply) => {
|
|
||||||
try {
|
|
||||||
if (req.session)
|
if (req.session)
|
||||||
await auth.invalidateSession(req.session.id)
|
await fastify.lucia.invalidateSession(req.session.id)
|
||||||
|
|
||||||
const blank = auth.createBlankSessionCookie()
|
const blank = fastify.lucia.createBlankSessionCookie()
|
||||||
|
|
||||||
reply.setCookie(blank.name, blank.value, blank.attributes)
|
reply.setCookie(blank.name, blank.value, blank.attributes)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
export default plugin
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
fastify.log.error(err)
|
|
||||||
reply.code(400).send({ error: err.message })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
111
server/routes/chat.ts
Normal file
111
server/routes/chat.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
|
||||||
|
import { Type } from 'typebox'
|
||||||
|
import { ChatMessageSchema, NewChatMessageSchema } from '../schemas/chat.ts'
|
||||||
|
|
||||||
|
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||||
|
fastify.post(
|
||||||
|
'/chat/send',
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
summary: 'Send message',
|
||||||
|
tags: ['Chat'],
|
||||||
|
operationId: 'chat.send',
|
||||||
|
body: NewChatMessageSchema,
|
||||||
|
response: {
|
||||||
|
200: ChatMessageSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (req, reply) => {
|
||||||
|
const user = req.user!
|
||||||
|
|
||||||
|
const message = await fastify.prisma.message.create({
|
||||||
|
data: {
|
||||||
|
text: req.body.text,
|
||||||
|
senderId: user.id,
|
||||||
|
attachments: {
|
||||||
|
create: (req.body.attachments ?? []).map((attachmentId) => {
|
||||||
|
return {
|
||||||
|
attachment: {
|
||||||
|
connect: {
|
||||||
|
id: attachmentId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return reply.unprocessableEntity()
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
id: message.id,
|
||||||
|
senderId: user.id,
|
||||||
|
text: message.text,
|
||||||
|
createdAt: message.createdAt.toISOString(),
|
||||||
|
updatedAt: message.updatedAt.toISOString(),
|
||||||
|
attachments: req.body.attachments ?? [],
|
||||||
|
}
|
||||||
|
|
||||||
|
fastify.bus.emit('chat:new-message', response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fastify.get(
|
||||||
|
'/chat',
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
summary: 'Get messages',
|
||||||
|
tags: ['Chat'],
|
||||||
|
operationId: 'chat.messages',
|
||||||
|
querystring: Type.Object({
|
||||||
|
cursor: Type.Optional(Type.String({ format: 'uuid', description: 'Cursor to message' })),
|
||||||
|
limit: Type.Number({ minimum: 1, maximum: 100, default: 10 }),
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: Type.Object({
|
||||||
|
messages: Type.Array(ChatMessageSchema),
|
||||||
|
nextCursor: Type.Optional(Type.String({ format: 'uuid', description: 'Cursor to last message' })),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line ts/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
async (req) => {
|
||||||
|
const messages = await fastify.prisma.message.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: req.query.limit + 1,
|
||||||
|
include: { attachments: true },
|
||||||
|
...(req.query.cursor && {
|
||||||
|
cursor: {
|
||||||
|
id: req.query.cursor,
|
||||||
|
},
|
||||||
|
// skip: 1,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasMore = messages.length > req.query.limit
|
||||||
|
const cursorMessage = hasMore ? messages.pop() : undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages: messages.map((message) => {
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
createdAt: message.createdAt.toISOString(),
|
||||||
|
updatedAt: message.updatedAt.toISOString(),
|
||||||
|
attachments: message.attachments.map(({ attachmentId }) => attachmentId),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
nextCursor: cursorMessage?.id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default plugin
|
||||||
@@ -1,97 +1,158 @@
|
|||||||
import type { FastifyInstance } from 'fastify'
|
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
|
||||||
import type { Namespace } from '../types/webrtc.ts'
|
import { Type } from 'typebox'
|
||||||
import { z } from 'zod'
|
import { UserSchema } from '../schemas/auth.ts'
|
||||||
import prisma from '../prisma/client.ts'
|
import { UpdateUserPreferencesSchema, UserPreferencesSchema } from '../schemas/user.ts'
|
||||||
import { socketToClient } from '../utils/socket-to-client.ts'
|
|
||||||
|
|
||||||
export default function (fastify: FastifyInstance) {
|
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||||
fastify.get('/preferences', async (req, reply) => {
|
fastify.get(
|
||||||
if (req.user) {
|
'/user',
|
||||||
return prisma.userPreferences.findFirst({ where: { userId: req.user.id } })
|
{
|
||||||
}
|
schema: {
|
||||||
|
summary: 'Get user',
|
||||||
reply.code(401).send(false)
|
tags: ['User'],
|
||||||
|
operationId: 'user.get',
|
||||||
|
querystring: Type.Partial(Type.Object({
|
||||||
|
username: Type.String(),
|
||||||
|
})),
|
||||||
|
response: {
|
||||||
|
200: UserSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (req, reply) => {
|
||||||
|
const user = await fastify.prisma.user.findFirst({
|
||||||
|
where: { username: req.query.username },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
displayName: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.patch('/preferences', async (req, reply) => {
|
if (!user) {
|
||||||
if (!req.user) {
|
return reply.notFound('User not found')
|
||||||
reply.code(401).send(false)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return {
|
||||||
const schema = z.object({
|
...user,
|
||||||
toggleInputHotkey: z.string().optional(),
|
createdAt: user.createdAt.toISOString(),
|
||||||
toggleOutputHotkey: z.string().optional(),
|
}
|
||||||
volumes: z.record(z.string(), z.number()).optional(),
|
},
|
||||||
})
|
)
|
||||||
const input = schema.parse(req.body)
|
|
||||||
|
|
||||||
return prisma.userPreferences.upsert({
|
fastify.get(
|
||||||
where: { userId: req.user.id },
|
'/user/preferences',
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
summary: 'Get preferences',
|
||||||
|
tags: ['User'],
|
||||||
|
operationId: 'user.getPreferences',
|
||||||
|
response: {
|
||||||
|
200: UserPreferencesSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (req, reply) => {
|
||||||
|
const user = req.user!
|
||||||
|
|
||||||
|
const preferences = await fastify.prisma.userPreferences.upsert({
|
||||||
|
where: { userId: user.id },
|
||||||
|
create: { userId: user.id },
|
||||||
|
update: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!preferences) {
|
||||||
|
return reply.notFound('User preferences not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toggleInputHotkey: preferences.toggleInputHotkey || '',
|
||||||
|
toggleOutputHotkey: preferences.toggleOutputHotkey || '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fastify.patch(
|
||||||
|
'/user/preferences',
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
summary: 'Update preferences',
|
||||||
|
tags: ['User'],
|
||||||
|
operationId: 'user.updatePreferences',
|
||||||
|
body: UpdateUserPreferencesSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (req) => {
|
||||||
|
const user = req.user!
|
||||||
|
|
||||||
|
return fastify.prisma.userPreferences.upsert({
|
||||||
|
where: { userId: user.id },
|
||||||
create: {
|
create: {
|
||||||
userId: req.user.id,
|
userId: user.id,
|
||||||
...input,
|
...req.body,
|
||||||
},
|
},
|
||||||
update: input,
|
update: req.body,
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
catch (err) {
|
)
|
||||||
fastify.log.error(err)
|
|
||||||
reply.code(400)
|
|
||||||
|
|
||||||
if (err instanceof z.ZodError) {
|
fastify.patch(
|
||||||
reply.send({ error: z.prettifyError(err) })
|
'/profile',
|
||||||
}
|
{
|
||||||
else {
|
schema: {
|
||||||
reply.send({ error: err.message })
|
summary: 'Update profile',
|
||||||
}
|
tags: ['User'],
|
||||||
}
|
operationId: 'user.updateProfile',
|
||||||
})
|
body: Type.Object({
|
||||||
|
displayName: Type.String(),
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: UserSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (req, reply) => {
|
||||||
|
const user = req.user!
|
||||||
|
|
||||||
fastify.patch('/profile', async (req, reply) => {
|
const updatedUser = await fastify.prisma.user.update({
|
||||||
if (!req.user) {
|
where: { id: user.id },
|
||||||
reply.code(401).send(false)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const schema = z.object({
|
|
||||||
displayName: z.string().optional(),
|
|
||||||
})
|
|
||||||
const input = schema.parse(req.body)
|
|
||||||
|
|
||||||
const updatedUser = prisma.user.update({
|
|
||||||
where: { id: req.user.id },
|
|
||||||
data: {
|
data: {
|
||||||
displayName: input.displayName,
|
displayName: req.body.displayName,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
displayName: true,
|
||||||
|
createdAt: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const namespace: Namespace = fastify.io.of('/webrtc')
|
if (!updatedUser) {
|
||||||
const sockets = await namespace.fetchSockets()
|
return reply.notFound('User not found')
|
||||||
|
|
||||||
const found = sockets.find(socket => socket.data.joined && socket.data.userId === req.user!.id)
|
|
||||||
|
|
||||||
if (found) {
|
|
||||||
found.data.displayName = input.displayName
|
|
||||||
namespace.emit('clientChanged', found.id, socketToClient(found))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedUser
|
const response = {
|
||||||
|
...updatedUser,
|
||||||
|
createdAt: updatedUser.createdAt.toISOString(),
|
||||||
}
|
}
|
||||||
catch (err) {
|
|
||||||
fastify.log.error(err)
|
|
||||||
reply.code(400)
|
|
||||||
|
|
||||||
if (err instanceof z.ZodError) {
|
fastify.bus.emit('user:profile-updated', response)
|
||||||
reply.send({ error: z.prettifyError(err) })
|
|
||||||
}
|
// TODO: подписаться в webrtc
|
||||||
else {
|
// const namespace: Namespace = fastify.io.of('/webrtc')
|
||||||
reply.send({ error: err.message })
|
// const sockets = await namespace.fetchSockets()
|
||||||
}
|
//
|
||||||
}
|
// const found = sockets.find(socket => socket.data.joined && socket.data.userId === req.user!.id)
|
||||||
})
|
//
|
||||||
|
// if (found) {
|
||||||
|
// found.data.displayName = req.body.displayName
|
||||||
|
// namespace.emit('clientChanged', found.id, socketToClient(found))
|
||||||
|
// }
|
||||||
|
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default plugin
|
||||||
|
|||||||
11
server/schemas/attachment.ts
Normal file
11
server/schemas/attachment.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Type } from 'typebox'
|
||||||
|
|
||||||
|
export const AttachmentSchema = Type.Object({
|
||||||
|
id: Type.String(),
|
||||||
|
name: Type.String(),
|
||||||
|
mimetype: Type.String(),
|
||||||
|
size: Type.Number({ minimum: 0 }),
|
||||||
|
createdAt: Type.String({ format: 'date-time' }),
|
||||||
|
|
||||||
|
// message: Type.MessageAttachment(),
|
||||||
|
}, { title: 'Attachment', description: 'Attachment' })
|
||||||
13
server/schemas/auth.ts
Normal file
13
server/schemas/auth.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Type } from 'typebox'
|
||||||
|
|
||||||
|
export const UserSchema = Type.Object({
|
||||||
|
id: Type.String(),
|
||||||
|
username: Type.String(),
|
||||||
|
displayName: Type.String(),
|
||||||
|
createdAt: Type.String({ format: 'date-time' }),
|
||||||
|
}, { title: 'User', description: 'User' })
|
||||||
|
|
||||||
|
export const CreateUserSchema = Type.Object({
|
||||||
|
username: Type.String({ minLength: 1 }),
|
||||||
|
password: Type.String({ minLength: 6 }),
|
||||||
|
}, { title: 'CreateUser' })
|
||||||
26
server/schemas/chat.ts
Normal file
26
server/schemas/chat.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Type } from 'typebox'
|
||||||
|
|
||||||
|
export const ReplySchema = Type.Object({
|
||||||
|
messageId: Type.String({ format: 'uuid' }),
|
||||||
|
senderId: Type.String({ format: 'uuid' }),
|
||||||
|
text: Type.String(),
|
||||||
|
}, { title: 'Reply', description: 'Reply' })
|
||||||
|
|
||||||
|
export const ChatMessageSchema = Type.Object({
|
||||||
|
id: Type.String({ format: 'uuid' }),
|
||||||
|
senderId: Type.String({ format: 'uuid' }),
|
||||||
|
text: Type.String({ minLength: 1 }),
|
||||||
|
createdAt: Type.String({ format: 'date-time' }),
|
||||||
|
updatedAt: Type.String({ format: 'date-time' }),
|
||||||
|
|
||||||
|
attachments: Type.Array(Type.String({ format: 'uuid' })),
|
||||||
|
// replyTo: ReplySchema,
|
||||||
|
}, { title: 'ChatMessage', description: 'ChatMessage' })
|
||||||
|
|
||||||
|
export const NewChatMessageSchema = Type.Object({
|
||||||
|
text: Type.String({ minLength: 1 }),
|
||||||
|
attachments: Type.Optional(Type.Array(Type.String({ format: 'uuid' }))),
|
||||||
|
// replyTo: Type.Object({
|
||||||
|
// messageId: Type.String({ format: 'uuid' }),
|
||||||
|
// }),
|
||||||
|
}, { title: 'NewChatMessage', description: 'NewChatMessage' })
|
||||||
7
server/schemas/common.ts
Normal file
7
server/schemas/common.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Type } from 'typebox'
|
||||||
|
|
||||||
|
export const ErrorReplySchema = Type.Object({
|
||||||
|
statusCode: Type.Number(),
|
||||||
|
error: Type.String(),
|
||||||
|
message: Type.String(),
|
||||||
|
}, { title: 'ResponseError', description: 'Response Error' })
|
||||||
11
server/schemas/user.ts
Normal file
11
server/schemas/user.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Type } from 'typebox'
|
||||||
|
|
||||||
|
export const UserPreferencesSchema = Type.Object({
|
||||||
|
toggleInputHotkey: Type.String(),
|
||||||
|
toggleOutputHotkey: Type.String(),
|
||||||
|
}, { title: 'UserPreferences', description: 'UserPreferences' })
|
||||||
|
|
||||||
|
export const UpdateUserPreferencesSchema = Type.Partial(
|
||||||
|
UserPreferencesSchema,
|
||||||
|
{ title: 'UpdateUserPreferences', description: 'UpdateUserPreferences' },
|
||||||
|
)
|
||||||
@@ -1,19 +1,80 @@
|
|||||||
|
import type { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
|
||||||
import { dirname, join } from 'node:path'
|
import { dirname, join } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import FastifyAutoLoad from '@fastify/autoload'
|
import FastifyAutoLoad from '@fastify/autoload'
|
||||||
import FastifyCookie from '@fastify/cookie'
|
import FastifyCookie from '@fastify/cookie'
|
||||||
import FastifyCors from '@fastify/cors'
|
import FastifyCors from '@fastify/cors'
|
||||||
import FastifyMultipart from '@fastify/multipart'
|
import FastifyMultipart from '@fastify/multipart'
|
||||||
|
import FastifySensible from '@fastify/sensible'
|
||||||
|
import FastifySwagger from '@fastify/swagger'
|
||||||
|
import FastifyApiReference from '@scalar/fastify-api-reference'
|
||||||
import Fastify from 'fastify'
|
import Fastify from 'fastify'
|
||||||
import prisma from './prisma/client.ts'
|
import { Prisma } from './prisma/generated-client/client.ts'
|
||||||
|
import { ErrorReplySchema } from './schemas/common.ts'
|
||||||
console.log(process.env.DATABASE_URL)
|
import 'dotenv/config'
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = dirname(__filename)
|
const __dirname = dirname(__filename)
|
||||||
|
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
logger: true,
|
logger: true,
|
||||||
|
}).withTypeProvider<TypeBoxTypeProvider>()
|
||||||
|
|
||||||
|
fastify.register(FastifySensible)
|
||||||
|
|
||||||
|
fastify.setErrorHandler((error, request, reply) => {
|
||||||
|
if (error instanceof Prisma.PrismaClientValidationError) {
|
||||||
|
reply.notAcceptable()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.statusCode) {
|
||||||
|
reply.getHttpError(error.statusCode, error.message)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.badRequest(error.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
fastify.register(FastifySwagger, {
|
||||||
|
openapi: {
|
||||||
|
info: {
|
||||||
|
version: '1.0.0',
|
||||||
|
title: 'Chad API',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transform: ({ schema, url }) => {
|
||||||
|
if (!url.startsWith('/chad'))
|
||||||
|
return { schema, url }
|
||||||
|
|
||||||
|
const transformedSchema: typeof schema = schema ?? {}
|
||||||
|
const responseSchema: any = transformedSchema.response ?? {}
|
||||||
|
|
||||||
|
responseSchema['4xx'] ??= ErrorReplySchema
|
||||||
|
responseSchema['5xx'] ??= ErrorReplySchema
|
||||||
|
|
||||||
|
transformedSchema.response = responseSchema
|
||||||
|
|
||||||
|
return { schema: transformedSchema, url }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
fastify.register(FastifyApiReference, {
|
||||||
|
routePrefix: '/reference',
|
||||||
|
configuration: {
|
||||||
|
showOperationId: true,
|
||||||
|
showDeveloperTools: 'never',
|
||||||
|
pageTitle: 'Chad API',
|
||||||
|
customCss: `
|
||||||
|
.scalar-mcp-layer,
|
||||||
|
.agent-button-container,
|
||||||
|
.t-doc__sidebar > div > button:last-child {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.register(FastifyCors, {
|
fastify.register(FastifyCors, {
|
||||||
@@ -44,9 +105,6 @@ fastify.register(FastifyAutoLoad, {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await fastify.listen({ port, host: '0.0.0.0' })
|
await fastify.listen({ port, host: '0.0.0.0' })
|
||||||
|
|
||||||
await prisma.$connect()
|
|
||||||
fastify.log.info('Testing DB Connection. OK')
|
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
fastify.log.error(err)
|
fastify.log.error(err)
|
||||||
|
|||||||
@@ -1,28 +1,7 @@
|
|||||||
import type { Server as SocketServer } from 'socket.io'
|
import type { Server as SocketServer } from 'socket.io'
|
||||||
import type { ChatClientMessage, ChatMessage } from '../types/chat.ts'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
|
|
||||||
export default async function (io: SocketServer) {
|
export default async function (io: SocketServer) {
|
||||||
const messages: ChatMessage[] = []
|
|
||||||
|
|
||||||
io.on('connection', async (socket) => {
|
io.on('connection', async (socket) => {
|
||||||
socket.on('chat:message', async (clientMessage: ChatClientMessage, cb) => {
|
|
||||||
const message: ChatMessage = {
|
|
||||||
id: uuidv4(),
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
sender: socket.data.username,
|
|
||||||
text: clientMessage.text,
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(message)
|
|
||||||
|
|
||||||
messages.push(message)
|
|
||||||
|
|
||||||
if (messages.length > 5000) {
|
|
||||||
messages.shift()
|
|
||||||
}
|
|
||||||
|
|
||||||
io.emit('chat:new-message', message)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { types } from 'mediasoup'
|
import type { types } from 'mediasoup'
|
||||||
import type { Server as SocketServer } from 'socket.io'
|
import type { Server as SocketServer } from 'socket.io'
|
||||||
|
import type { PrismaClient } from '../prisma/generated-client/client.ts'
|
||||||
import type {
|
import type {
|
||||||
ChadClient,
|
ChadClient,
|
||||||
SomeSocket,
|
SomeSocket,
|
||||||
} from '../types/webrtc.ts'
|
} from '../types/webrtc.ts'
|
||||||
import { consola } from 'consola'
|
import { consola } from 'consola'
|
||||||
import prisma from '../prisma/client.ts'
|
|
||||||
import { socketToClient } from '../utils/socket-to-client.ts'
|
import { socketToClient } from '../utils/socket-to-client.ts'
|
||||||
|
|
||||||
export default async function (io: SocketServer, router: types.Router) {
|
export default async function (io: SocketServer, router: types.Router, prisma: PrismaClient) {
|
||||||
const audioLevelObserver = await router.createAudioLevelObserver({
|
const audioLevelObserver = await router.createAudioLevelObserver({
|
||||||
maxEntries: 10,
|
maxEntries: 10,
|
||||||
threshold: -80,
|
threshold: -80,
|
||||||
|
|||||||
BIN
server/uploads/25fad78d-59b1-4e74-8a71-f1c661b3bbfe
Normal file
BIN
server/uploads/25fad78d-59b1-4e74-8a71-f1c661b3bbfe
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
BIN
server/uploads/6563029c-b0b4-4010-87b8-27ba0b841290
Normal file
BIN
server/uploads/6563029c-b0b4-4010-87b8-27ba0b841290
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
BIN
server/uploads/664e8db8-05af-465c-9bd0-d196f3dc1563
Normal file
BIN
server/uploads/664e8db8-05af-465c-9bd0-d196f3dc1563
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
BIN
server/uploads/c7c0ae6d-593d-4d17-a43b-c9415d698576
Normal file
BIN
server/uploads/c7c0ae6d-593d-4d17-a43b-c9415d698576
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
BIN
server/uploads/d56501cb-c6d2-4af1-88a9-5ba75479b356
Normal file
BIN
server/uploads/d56501cb-c6d2-4af1-88a9-5ba75479b356
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
1471
server/yarn.lock
1471
server/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user