вложения, канальчики, бим-бим + бам-бам
This commit is contained in:
@@ -16,7 +16,7 @@ export const useAuth = createGlobalState(() => {
|
||||
|
||||
async function login(username: string, password: string): Promise<void> {
|
||||
try {
|
||||
const result = await chadApi<Me>('/login', {
|
||||
const result = await chadApi<Me>('/auth/login', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
username,
|
||||
@@ -33,7 +33,7 @@ export const useAuth = createGlobalState(() => {
|
||||
|
||||
async function register(username: string, password: string): Promise<void> {
|
||||
try {
|
||||
const result = await chadApi<Me>('/register', {
|
||||
const result = await chadApi<Me>('/auth/register', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
username,
|
||||
@@ -50,7 +50,7 @@ export const useAuth = createGlobalState(() => {
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
try {
|
||||
await chadApi('/logout', { method: 'POST' })
|
||||
await chadApi('/auth/logout', { method: 'POST' })
|
||||
|
||||
setMe(undefined)
|
||||
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import chadApi from '#shared/chad-api'
|
||||
import { createGlobalState } from '@vueuse/core'
|
||||
|
||||
export interface ChatClientMessage {
|
||||
text: string
|
||||
replyTo?: {
|
||||
messageId: string
|
||||
}
|
||||
// replyTo?: {
|
||||
// messageId: string
|
||||
// }
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string
|
||||
sender: string
|
||||
senderId: string
|
||||
text: string
|
||||
createdAt: string
|
||||
replyTo?: {
|
||||
messageId: string
|
||||
sender: string
|
||||
text: string
|
||||
}
|
||||
updatedAt: string
|
||||
attachments: string[]
|
||||
// replyTo?: {
|
||||
// messageId: string
|
||||
// sender: string
|
||||
// text: string
|
||||
// }
|
||||
}
|
||||
|
||||
export const useChat = createGlobalState(() => {
|
||||
@@ -41,16 +44,16 @@ export const useChat = createGlobalState(() => {
|
||||
})
|
||||
}, { immediate: true, flush: 'sync' })
|
||||
|
||||
function sendMessage(message: ChatClientMessage) {
|
||||
if (!signaling.connected.value)
|
||||
return
|
||||
|
||||
async function sendMessage(message: ChatClientMessage) {
|
||||
message.text = message.text.trim()
|
||||
|
||||
if (!message.text.length)
|
||||
return
|
||||
|
||||
signaling.socket.value!.emit('chat:message', message)
|
||||
await chadApi<ChatMessage>('/chat/send', {
|
||||
method: 'POST',
|
||||
body: message,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -42,7 +42,7 @@ export const usePreferences = createGlobalState(() => {
|
||||
async ([toggleInputHotkey, toggleOutputHotkey]) => {
|
||||
try {
|
||||
await chadApi(
|
||||
'/preferences',
|
||||
'/user/preferences',
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
|
||||
@@ -5,7 +5,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
|
||||
if (!me.value) {
|
||||
try {
|
||||
setMe(await chadApi('/me', { method: 'GET' }))
|
||||
setMe(await chadApi('/auth/me', { method: 'GET' }))
|
||||
|
||||
if (to.meta.auth !== false)
|
||||
return navigateTo({ name: 'Index' })
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineNuxtRouteMiddleware(async () => {
|
||||
return
|
||||
|
||||
try {
|
||||
const preferences = await chadApi<SyncedPreferences>('/preferences', { method: 'GET' })
|
||||
const preferences = await chadApi<SyncedPreferences>('/user/preferences', { method: 'GET' })
|
||||
|
||||
if (!preferences)
|
||||
return
|
||||
|
||||
@@ -10,25 +10,35 @@
|
||||
:key="message.id"
|
||||
class="w-fit max-w-[60%]"
|
||||
:class="{
|
||||
'ml-auto': message.sender === me?.username,
|
||||
'ml-auto': message.senderId === me?.userId,
|
||||
}"
|
||||
>
|
||||
<p
|
||||
v-if="message.sender !== me?.username"
|
||||
v-if="message.senderId !== me?.userId"
|
||||
class="text-sm text-muted-color mb-1"
|
||||
>
|
||||
{{ message.sender }}
|
||||
{{ message.senderId }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="px-3 py-2 rounded-lg"
|
||||
:class="{
|
||||
'bg-surface-800': message.sender !== me?.username,
|
||||
'bg-surface-700': message.sender === me?.username,
|
||||
'bg-surface-800 rounded-tl': message.senderId !== me?.userId,
|
||||
'bg-surface-700 rounded-tr': message.senderId === me?.userId,
|
||||
}"
|
||||
>
|
||||
<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')">
|
||||
{{ formatDate(message.createdAt) }}
|
||||
</p>
|
||||
|
||||
@@ -52,7 +52,7 @@ async function save() {
|
||||
|
||||
saving.value = true
|
||||
|
||||
const updatedMe = await chadApi('/profile', {
|
||||
const updatedMe = await chadApi('/user/profile', {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
displayName: displayName.value,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "Chad",
|
||||
"version": "0.3.0-rc.2",
|
||||
"version": "0.3.0-rc.3",
|
||||
"identifier": "xyz.koptilnya.chad",
|
||||
"build": {
|
||||
"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",
|
||||
"scripts": {
|
||||
"dev": "nodemon",
|
||||
"start": "ts-node --transpile-only server.ts",
|
||||
"db:deploy": "npx prisma migrate deploy && npx prisma generate"
|
||||
},
|
||||
@@ -11,8 +12,13 @@
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/cors": "^11.1.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",
|
||||
"@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",
|
||||
"consola": "^3.4.2",
|
||||
"dotenv": "^17.2.3",
|
||||
@@ -20,8 +26,9 @@
|
||||
"fastify-plugin": "^5.1.0",
|
||||
"lucia": "^3.2.2",
|
||||
"mediasoup": "^3.19.3",
|
||||
"prisma": "^6.17.0",
|
||||
"prisma": "7",
|
||||
"socket.io": "^4.8.1",
|
||||
"typebox": "^1.1.27",
|
||||
"uuid": "^13.0.0",
|
||||
"ws": "^8.18.3",
|
||||
"zod": "^4.1.12"
|
||||
@@ -29,8 +36,10 @@
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^5.4.1",
|
||||
"@types/bcrypt": "^6",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/ws": "^8",
|
||||
"eslint": "^9.36.0",
|
||||
"nodemon": "^3.1.14",
|
||||
"ts-node": "^10.9.2",
|
||||
"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 { 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' {
|
||||
interface FastifyInstance {
|
||||
lucia: Lucia
|
||||
}
|
||||
|
||||
interface FastifyRequest {
|
||||
user: User | null
|
||||
user: DatabaseUserAttributes | null
|
||||
session: Session | null
|
||||
}
|
||||
|
||||
interface FastifyContextConfig {
|
||||
skipAuth: boolean
|
||||
}
|
||||
}
|
||||
|
||||
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('session', null)
|
||||
|
||||
fastify.addHook('preHandler', async (req, reply) => {
|
||||
fastify.addHook('onRequest', async (req, reply) => {
|
||||
try {
|
||||
const sessionId = auth.readSessionCookie(req.headers.cookie ?? '')
|
||||
const sessionId = lucia.readSessionCookie(req.headers.cookie ?? '')
|
||||
|
||||
if (!sessionId)
|
||||
return
|
||||
|
||||
const { session, user } = await auth.validateSession(sessionId ?? '')
|
||||
const { session, user } = await lucia.validateSession(sessionId ?? '')
|
||||
|
||||
if (session && session.fresh) {
|
||||
const cookie = auth.createSessionCookie(session.id)
|
||||
const cookie = lucia.createSessionCookie(session.id)
|
||||
|
||||
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
const blank = auth.createBlankSessionCookie()
|
||||
const blank = lucia.createBlankSessionCookie()
|
||||
|
||||
reply.setCookie(blank.name, blank.value, blank.attributes)
|
||||
}
|
||||
|
||||
req.user = user
|
||||
req.user = user as DatabaseUserAttributes
|
||||
req.session = session
|
||||
}
|
||||
catch {
|
||||
@@ -42,4 +78,21 @@ export default fp(async (fastify) => {
|
||||
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 { ServerOptions } from 'socket.io'
|
||||
import type { MessageSelect } from '../prisma/generated-client/models/Message.ts'
|
||||
import fp from 'fastify-plugin'
|
||||
import { Server } from 'socket.io'
|
||||
import registerChatSocket from '../socket/chat.ts'
|
||||
@@ -23,9 +24,11 @@ export default fp<Partial<ServerOptions>>(
|
||||
await fastify.io.close()
|
||||
})
|
||||
|
||||
fastify.ready(async () => {
|
||||
await registerWebrtcSocket(fastify.io, fastify.mediasoupRouter)
|
||||
await registerWebrtcSocket(fastify.io, fastify.mediasoupRouter, fastify.prisma)
|
||||
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'] },
|
||||
|
||||
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 {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
// output = "./generated/client"
|
||||
provider = "prisma-client"
|
||||
output = "./generated-client"
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -18,6 +17,7 @@ model User {
|
||||
|
||||
Session Session[]
|
||||
UserPreferences UserPreferences?
|
||||
Messages Message[]
|
||||
}
|
||||
|
||||
model Session {
|
||||
@@ -31,10 +31,46 @@ model Session {
|
||||
}
|
||||
|
||||
model UserPreferences {
|
||||
userId String @id
|
||||
userId String @id @unique
|
||||
toggleInputHotkey 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 { z } from 'zod'
|
||||
import { auth } from '../auth/lucia.ts'
|
||||
import prisma from '../prisma/client.ts'
|
||||
import { Type } from 'typebox'
|
||||
import { CreateUserSchema, UserSchema } from '../schemas/auth.ts'
|
||||
|
||||
export default function (fastify: FastifyInstance) {
|
||||
fastify.post('/register', async (req, reply) => {
|
||||
try {
|
||||
const schema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(6),
|
||||
})
|
||||
const input = schema.parse(req.body)
|
||||
|
||||
const hashed = await bcrypt.hash(input.password, 10)
|
||||
const user = await prisma.user.create({
|
||||
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||
fastify.post(
|
||||
'/auth/register',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Register',
|
||||
tags: ['Auth'],
|
||||
operationId: 'auth.register',
|
||||
body: CreateUserSchema,
|
||||
response: {
|
||||
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: {
|
||||
username: input.username,
|
||||
username: req.body.username,
|
||||
password: hashed,
|
||||
displayName: input.username,
|
||||
displayName: req.body.username,
|
||||
UserPreferences: {
|
||||
create: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const session = await auth.createSession(user.id, {})
|
||||
const cookie = auth.createSessionCookie(session.id)
|
||||
const session = await fastify.lucia.createSession(user.id, {})
|
||||
const cookie = fastify.lucia.createSessionCookie(session.id)
|
||||
|
||||
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
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) {
|
||||
reply.send({ error: z.prettifyError(err) })
|
||||
}
|
||||
else {
|
||||
reply.send({ error: err.message })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
fastify.post('/login', async (req, reply) => {
|
||||
try {
|
||||
const schema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string(),
|
||||
})
|
||||
const input = schema.parse(req.body)
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { username: input.username },
|
||||
fastify.post(
|
||||
'/auth/login',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Login',
|
||||
tags: ['Auth'],
|
||||
operationId: 'auth.login',
|
||||
body: Type.Object({
|
||||
username: Type.String({ minLength: 1 }),
|
||||
password: Type.String({ minLength: 1 }),
|
||||
}),
|
||||
response: {
|
||||
200: UserSchema,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
skipAuth: true,
|
||||
},
|
||||
},
|
||||
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) {
|
||||
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) {
|
||||
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 cookie = auth.createSessionCookie(session.id)
|
||||
const session = await fastify.lucia.createSession(user.id, {})
|
||||
const cookie = fastify.lucia.createSessionCookie(session.id)
|
||||
|
||||
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 {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
}
|
||||
}
|
||||
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 })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
fastify.get('/me', async (req, reply) => {
|
||||
if (req.user) {
|
||||
return req.user
|
||||
}
|
||||
|
||||
reply.code(401).send(false)
|
||||
})
|
||||
|
||||
fastify.post('/logout', async (req, reply) => {
|
||||
try {
|
||||
fastify.post(
|
||||
'/auth/logout',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Logout',
|
||||
tags: ['Auth'],
|
||||
operationId: 'auth.logout',
|
||||
},
|
||||
},
|
||||
async (req, reply) => {
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
catch (err) {
|
||||
fastify.log.error(err)
|
||||
reply.code(400).send({ error: err.message })
|
||||
}
|
||||
})
|
||||
}
|
||||
export default plugin
|
||||
|
||||
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 { Namespace } from '../types/webrtc.ts'
|
||||
import { z } from 'zod'
|
||||
import prisma from '../prisma/client.ts'
|
||||
import { socketToClient } from '../utils/socket-to-client.ts'
|
||||
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
|
||||
import { Type } from 'typebox'
|
||||
import { UserSchema } from '../schemas/auth.ts'
|
||||
import { UpdateUserPreferencesSchema, UserPreferencesSchema } from '../schemas/user.ts'
|
||||
|
||||
export default function (fastify: FastifyInstance) {
|
||||
fastify.get('/preferences', async (req, reply) => {
|
||||
if (req.user) {
|
||||
return prisma.userPreferences.findFirst({ where: { userId: req.user.id } })
|
||||
}
|
||||
|
||||
reply.code(401).send(false)
|
||||
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||
fastify.get(
|
||||
'/user',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Get user',
|
||||
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 (!req.user) {
|
||||
reply.code(401).send(false)
|
||||
|
||||
return
|
||||
if (!user) {
|
||||
return reply.notFound('User not found')
|
||||
}
|
||||
|
||||
try {
|
||||
const schema = z.object({
|
||||
toggleInputHotkey: z.string().optional(),
|
||||
toggleOutputHotkey: z.string().optional(),
|
||||
volumes: z.record(z.string(), z.number()).optional(),
|
||||
})
|
||||
const input = schema.parse(req.body)
|
||||
return {
|
||||
...user,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return prisma.userPreferences.upsert({
|
||||
where: { userId: req.user.id },
|
||||
fastify.get(
|
||||
'/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: {
|
||||
userId: req.user.id,
|
||||
...input,
|
||||
userId: user.id,
|
||||
...req.body,
|
||||
},
|
||||
update: input,
|
||||
update: req.body,
|
||||
})
|
||||
}
|
||||
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 })
|
||||
}
|
||||
}
|
||||
})
|
||||
fastify.patch(
|
||||
'/profile',
|
||||
{
|
||||
schema: {
|
||||
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) => {
|
||||
if (!req.user) {
|
||||
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 },
|
||||
const updatedUser = await fastify.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
displayName: input.displayName,
|
||||
displayName: req.body.displayName,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
displayName: true,
|
||||
createdAt: true,
|
||||
},
|
||||
})
|
||||
|
||||
const namespace: Namespace = fastify.io.of('/webrtc')
|
||||
const sockets = await namespace.fetchSockets()
|
||||
|
||||
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))
|
||||
if (!updatedUser) {
|
||||
return reply.notFound('User not found')
|
||||
}
|
||||
|
||||
return updatedUser
|
||||
const response = {
|
||||
...updatedUser,
|
||||
createdAt: updatedUser.createdAt.toISOString(),
|
||||
}
|
||||
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 })
|
||||
}
|
||||
}
|
||||
})
|
||||
fastify.bus.emit('user:profile-updated', response)
|
||||
|
||||
// TODO: подписаться в webrtc
|
||||
// const namespace: Namespace = fastify.io.of('/webrtc')
|
||||
// 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 { fileURLToPath } from 'node:url'
|
||||
import FastifyAutoLoad from '@fastify/autoload'
|
||||
import FastifyCookie from '@fastify/cookie'
|
||||
import FastifyCors from '@fastify/cors'
|
||||
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 prisma from './prisma/client.ts'
|
||||
|
||||
console.log(process.env.DATABASE_URL)
|
||||
import { Prisma } from './prisma/generated-client/client.ts'
|
||||
import { ErrorReplySchema } from './schemas/common.ts'
|
||||
import 'dotenv/config'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
const fastify = Fastify({
|
||||
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, {
|
||||
@@ -44,9 +105,6 @@ fastify.register(FastifyAutoLoad, {
|
||||
|
||||
try {
|
||||
await fastify.listen({ port, host: '0.0.0.0' })
|
||||
|
||||
await prisma.$connect()
|
||||
fastify.log.info('Testing DB Connection. OK')
|
||||
}
|
||||
catch (err) {
|
||||
fastify.log.error(err)
|
||||
|
||||
@@ -1,28 +1,7 @@
|
||||
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) {
|
||||
const messages: ChatMessage[] = []
|
||||
|
||||
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 { Server as SocketServer } from 'socket.io'
|
||||
import type { PrismaClient } from '../prisma/generated-client/client.ts'
|
||||
import type {
|
||||
ChadClient,
|
||||
SomeSocket,
|
||||
} from '../types/webrtc.ts'
|
||||
import { consola } from 'consola'
|
||||
import prisma from '../prisma/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({
|
||||
maxEntries: 10,
|
||||
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