mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Refactor APIs
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { API_BASE_URL } from "@/core/config";
|
import { API_BASE_URL } from "@/core/config";
|
||||||
import { getAuthHeaders } from "@/core/api/authApi";
|
import { getAuthHeaders } from "./index";
|
||||||
|
|
||||||
export interface DeviceInfo {
|
export interface DeviceInfo {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
@@ -18,20 +18,19 @@ export interface DeviceInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function listDevices(token: string): Promise<DeviceInfo[]> {
|
export async function listDevices(token: string): Promise<DeviceInfo[]> {
|
||||||
const res = await fetch(`${API_BASE_URL}/devices`, { headers: getAuthHeaders(token) });
|
const res = await fetch(`${API_BASE_URL}/devices`, { headers: getAuthHeaders(token, true) });
|
||||||
if (!res.ok) throw new Error("Failed to fetch devices");
|
if (!res.ok) throw new Error("Failed to fetch devices");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data.devices as DeviceInfo[];
|
return data.devices as DeviceInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function revokeDevice(token: string, sessionId: string): Promise<void> {
|
export async function revokeDevice(token: string, sessionId: string): Promise<void> {
|
||||||
const res = await fetch(`${API_BASE_URL}/devices/${sessionId}`, { method: "DELETE", headers: getAuthHeaders(token) });
|
const res = await fetch(`${API_BASE_URL}/devices/${sessionId}`, { method: "DELETE", headers: getAuthHeaders(token, true) });
|
||||||
if (!res.ok) throw new Error("Failed to revoke device");
|
if (!res.ok) throw new Error("Failed to revoke device");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logoutAllOtherDevices(token: string): Promise<void> {
|
export async function logoutAllOtherDevices(token: string): Promise<void> {
|
||||||
const res = await fetch(`${API_BASE_URL}/devices/logout-all`, { method: "POST", headers: getAuthHeaders(token) });
|
const res = await fetch(`${API_BASE_URL}/devices/logout-all`, { method: "POST", headers: getAuthHeaders(token, true) });
|
||||||
if (!res.ok) throw new Error("Failed to logout all devices");
|
if (!res.ok) throw new Error("Failed to logout all devices");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import type { Headers, UploadPublicKeyRequest, BackupBlob } from "@/core/types";
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import type { LoginRequest, RegisterRequest, LoginResponse } from "@/core/types";
|
||||||
import { generateX25519KeyPair } from "@/utils/crypto/asymmetric";
|
import { generateX25519KeyPair } from "@/utils/crypto/asymmetric";
|
||||||
import { encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "@/utils/crypto/backup";
|
import { encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "@/utils/crypto/backup";
|
||||||
import { b64, ub64 } from "@/utils/utils";
|
import { b64, ub64 } from "@/utils/utils";
|
||||||
import { API_BASE_URL } from "@/core/config";
|
|
||||||
import { hkdfExtractAndExpand } from "@/utils/crypto/kdf";
|
import { hkdfExtractAndExpand } from "@/utils/crypto/kdf";
|
||||||
|
import { fetchPublicKey, uploadPublicKey, fetchBackupBlob, uploadBackupBlob } from "../crypto";
|
||||||
|
import type { Headers } from "@/core/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates authentication headers for API requests
|
* Generates authentication headers for API requests
|
||||||
|
* @param {string | null} token - Authentication token
|
||||||
* @param {boolean} json - Whether to include JSON content type header
|
* @param {boolean} json - Whether to include JSON content type header
|
||||||
* @returns {Headers} Headers object with authentication and content type
|
* @returns {Headers} Headers object with authentication and content type
|
||||||
*/
|
*/
|
||||||
@@ -23,54 +26,15 @@ export function getAuthHeaders(token: string | null, json: boolean = true): Head
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentPublicKey: Uint8Array | null = null;
|
export interface CheckAuthResponse {
|
||||||
let currentPrivateKey: Uint8Array | null = null;
|
authenticated: boolean;
|
||||||
|
username: string;
|
||||||
async function fetchPublicKey(token: string): Promise<Uint8Array | null> {
|
admin: boolean;
|
||||||
const headers = getAuthHeaders(token, true);
|
|
||||||
const res = await fetch(`${API_BASE_URL}/crypto/public-key`, { method: "GET", headers });
|
|
||||||
if (!res.ok) return null;
|
|
||||||
const data = await res.json();
|
|
||||||
if (!data?.publicKey) return null;
|
|
||||||
return ub64(data.publicKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadPublicKey(publicKey: Uint8Array, token: string): Promise<void> {
|
export interface LogoutResponse {
|
||||||
const payload: UploadPublicKeyRequest = {
|
status: string;
|
||||||
publicKey: b64(publicKey)
|
message: string;
|
||||||
}
|
|
||||||
|
|
||||||
const headers = getAuthHeaders(token, true);
|
|
||||||
await fetch(`${API_BASE_URL}/crypto/public-key`, {
|
|
||||||
method: "POST",
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchBackupBlob(token: string): Promise<string | null> {
|
|
||||||
const headers = getAuthHeaders(token, true);
|
|
||||||
const res = await fetch(`${API_BASE_URL}/crypto/backup`, {
|
|
||||||
method: "GET",
|
|
||||||
headers
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
const response: BackupBlob = await res.json();
|
|
||||||
return response.blob;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadBackupBlob(blobJson: string, token: string): Promise<void> {
|
|
||||||
const payload: BackupBlob = { blob: blobJson }
|
|
||||||
|
|
||||||
const headers = getAuthHeaders(token, true);
|
|
||||||
await fetch(`${API_BASE_URL}/crypto/backup`, {
|
|
||||||
method: "POST",
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserKeyPairMemory {
|
export interface UserKeyPairMemory {
|
||||||
@@ -78,6 +42,9 @@ export interface UserKeyPairMemory {
|
|||||||
privateKey: Uint8Array;
|
privateKey: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentPublicKey: Uint8Array | null = null;
|
||||||
|
let currentPrivateKey: Uint8Array | null = null;
|
||||||
|
|
||||||
export function getCurrentKeys(): UserKeyPairMemory | null {
|
export function getCurrentKeys(): UserKeyPairMemory | null {
|
||||||
if (currentPublicKey && currentPrivateKey) return { publicKey: currentPublicKey, privateKey: currentPrivateKey };
|
if (currentPublicKey && currentPrivateKey) return { publicKey: currentPublicKey, privateKey: currentPrivateKey };
|
||||||
return null;
|
return null;
|
||||||
@@ -94,6 +61,72 @@ function saveKeys(
|
|||||||
localStorage.setItem("privateKey", encodedPrivateKey);
|
localStorage.setItem("privateKey", encodedPrivateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current user is authenticated
|
||||||
|
*/
|
||||||
|
export async function checkAuth(token: string): Promise<CheckAuthResponse> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/check_auth`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to check auth");
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs in a user with username and password
|
||||||
|
*/
|
||||||
|
export async function login(request: LoginRequest): Promise<LoginResponse> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: getAuthHeaders(null, true),
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ detail: "Login failed" }));
|
||||||
|
throw new Error(error.detail || "Login failed");
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a new user
|
||||||
|
*/
|
||||||
|
export async function register(request: RegisterRequest): Promise<LoginResponse> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/register`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: getAuthHeaders(null, true),
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ detail: "Registration failed" }));
|
||||||
|
throw new Error(error.detail || "Registration failed");
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs out the current user
|
||||||
|
*/
|
||||||
|
export async function logout(token: string): Promise<LogoutResponse> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/logout`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to logout");
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive a client-side authentication secret so the raw password never leaves the client.
|
||||||
|
* Uses PBKDF2 (via WebCrypto) + HKDF to produce a stable 32-byte key, then base64.
|
||||||
|
*/
|
||||||
|
export async function deriveAuthSecret(username: string, password: string): Promise<string> {
|
||||||
|
// Use per-user salt derived from username; in future we can fetch a server-provided salt
|
||||||
|
const salt = new TextEncoder().encode(`fromchat.user:${username}`);
|
||||||
|
// Derive 32 bytes using HKDF; PBKDF2 already used within importPassword
|
||||||
|
const derived = await hkdfExtractAndExpand(new TextEncoder().encode(password), salt, new TextEncoder().encode("auth-secret"), 32);
|
||||||
|
return b64(derived);
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureKeysOnLogin(password: string, token: string): Promise<UserKeyPairMemory> {
|
export async function ensureKeysOnLogin(password: string, token: string): Promise<UserKeyPairMemory> {
|
||||||
// Try to restore from backup
|
// Try to restore from backup
|
||||||
const blobJson = await fetchBackupBlob(token);
|
const blobJson = await fetchBackupBlob(token);
|
||||||
@@ -147,13 +180,41 @@ export function getAuthToken(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive a client-side authentication secret so the raw password never leaves the client.
|
* Changes the user's password
|
||||||
* Uses PBKDF2 (via WebCrypto) + HKDF to produce a stable 32-byte key, then base64.
|
|
||||||
*/
|
*/
|
||||||
export async function deriveAuthSecret(username: string, password: string): Promise<string> {
|
export async function changePassword(
|
||||||
// Use per-user salt derived from username; in future we can fetch a server-provided salt
|
token: string,
|
||||||
const salt = new TextEncoder().encode(`fromchat.user:${username}`);
|
username: string,
|
||||||
// Derive 32 bytes using HKDF; PBKDF2 already used within importPassword
|
currentPassword: string,
|
||||||
const derived = await hkdfExtractAndExpand(new TextEncoder().encode(password), salt, new TextEncoder().encode("auth-secret"), 32);
|
newPassword: string,
|
||||||
return b64(derived);
|
logoutAllExceptCurrent: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
const currentDerived = await deriveAuthSecret(username, currentPassword);
|
||||||
|
const newDerived = await deriveAuthSecret(username, newPassword);
|
||||||
|
const res = await fetch(`${API_BASE_URL}/change-password`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: getAuthHeaders(token, true),
|
||||||
|
body: JSON.stringify({
|
||||||
|
currentPasswordDerived: currentDerived,
|
||||||
|
newPasswordDerived: newDerived,
|
||||||
|
logoutAllExceptCurrent
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to change password");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the current user's account
|
||||||
|
*/
|
||||||
|
export async function deleteAccount(token: string): Promise<{ status: string; message: string }> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/account/delete`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ detail: "Failed to delete account" }));
|
||||||
|
throw new Error(error.detail || "Failed to delete account");
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
import { getAuthHeaders } from ".";
|
||||||
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import type { UserProfile } from "@/core/types";
|
||||||
|
|
||||||
|
export interface ProfileData {
|
||||||
|
profile_picture?: string;
|
||||||
|
username?: string;
|
||||||
|
display_name?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadResponse {
|
||||||
|
profile_picture_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads user profile data from the server
|
||||||
|
*/
|
||||||
|
export async function loadProfile(token: string): Promise<ProfileData | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/profile`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
// Map backend fields to frontend fields
|
||||||
|
return {
|
||||||
|
profile_picture: data.profile_picture,
|
||||||
|
username: data.username,
|
||||||
|
display_name: data.display_name,
|
||||||
|
description: data.bio
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading profile:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a profile picture to the server
|
||||||
|
*/
|
||||||
|
export async function uploadProfilePicture(token: string, file: Blob): Promise<UploadResponse | null> {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('profile_picture', file, 'profile_picture.jpg');
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/upload-profile-picture`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: getAuthHeaders(token, false)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates user profile information
|
||||||
|
*/
|
||||||
|
export async function updateProfile(token: string, data: Partial<ProfileData>): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Map frontend fields to backend fields
|
||||||
|
const backendData = {
|
||||||
|
username: data.username,
|
||||||
|
display_name: data.display_name,
|
||||||
|
description: data.description
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/profile`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
...getAuthHeaders(token, true),
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(backendData)
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating profile:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates user bio
|
||||||
|
*/
|
||||||
|
export async function updateBio(token: string, bio: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/bio`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: getAuthHeaders(token, true),
|
||||||
|
body: JSON.stringify({ bio })
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating bio:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches user profile data by username
|
||||||
|
*/
|
||||||
|
export async function fetchUserProfile(token: string, username: string): Promise<UserProfile | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/${username}`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user profile:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches user profile data by user ID
|
||||||
|
*/
|
||||||
|
export async function fetchUserProfileById(token: string, userId: number): Promise<UserProfile | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/id/${userId}`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user profile by ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles verification status for a user (owner only)
|
||||||
|
*/
|
||||||
|
export async function verifyUser(userId: number, token: string): Promise<{verified: boolean} | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/${userId}/verify`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error verifying user:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory cache for user similarity results
|
||||||
|
* Key: userId, Value: similarity result
|
||||||
|
*/
|
||||||
|
const similarityCache = new Map<number, {isSimilar: boolean, similarTo?: string} | null>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a user is similar to any verified user
|
||||||
|
* Results are cached in memory to avoid redundant API calls
|
||||||
|
*/
|
||||||
|
export async function checkUserSimilarity(userId: number, token: string): Promise<{isSimilar: boolean, similarTo?: string} | null> {
|
||||||
|
// Check cache first
|
||||||
|
if (similarityCache.has(userId)) {
|
||||||
|
return similarityCache.get(userId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/check-similarity/${userId}`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
|
||||||
|
let result: {isSimilar: boolean, similarTo?: string} | null = null;
|
||||||
|
if (response.ok) {
|
||||||
|
result = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the result (even if null/error)
|
||||||
|
similarityCache.set(userId, result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking user similarity:', error);
|
||||||
|
const result: null = null;
|
||||||
|
// Cache null result to avoid retrying on errors
|
||||||
|
similarityCache.set(userId, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suspends a user account (admin only)
|
||||||
|
*/
|
||||||
|
export async function suspendUser(userId: number, reason: string, token: string): Promise<{status: string; message: string; reason: string} | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/${userId}/suspend`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(token, true),
|
||||||
|
body: JSON.stringify({ reason })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error suspending user:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsuspends a user account (admin only)
|
||||||
|
*/
|
||||||
|
export async function unsuspendUser(userId: number, token: string): Promise<{status: string; message: string} | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/${userId}/unsuspend`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unsuspending user:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a user account (admin only)
|
||||||
|
*/
|
||||||
|
export async function deleteUser(userId: number, token: string): Promise<{status: string; message: string} | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/${userId}/delete`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting user:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import { getAuthHeaders } from "./account";
|
||||||
|
import type { UploadPublicKeyRequest, BackupBlob } from "@/core/types";
|
||||||
|
import { b64, ub64 } from "@/utils/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the current user's public key
|
||||||
|
*/
|
||||||
|
export async function fetchPublicKey(token: string): Promise<Uint8Array | null> {
|
||||||
|
const headers = getAuthHeaders(token, true);
|
||||||
|
const res = await fetch(`${API_BASE_URL}/crypto/public-key`, { method: "GET", headers });
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data?.publicKey) return null;
|
||||||
|
return ub64(data.publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads the current user's public key
|
||||||
|
*/
|
||||||
|
export async function uploadPublicKey(publicKey: Uint8Array, token: string): Promise<void> {
|
||||||
|
const payload: UploadPublicKeyRequest = {
|
||||||
|
publicKey: b64(publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = getAuthHeaders(token, true);
|
||||||
|
const res = await fetch(`${API_BASE_URL}/crypto/public-key`, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to upload public key");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches another user's public key by user ID
|
||||||
|
*/
|
||||||
|
export async function fetchUserPublicKey(userId: number, token: string): Promise<string | null> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/crypto/public-key/of/${userId}`, { headers: getAuthHeaders(token, true) });
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const data = await res.json();
|
||||||
|
return data.publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the current user's backup blob
|
||||||
|
*/
|
||||||
|
export async function fetchBackupBlob(token: string): Promise<string | null> {
|
||||||
|
const headers = getAuthHeaders(token, true);
|
||||||
|
const res = await fetch(`${API_BASE_URL}/crypto/backup`, {
|
||||||
|
method: "GET",
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const response: BackupBlob = await res.json();
|
||||||
|
return response.blob;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads the current user's backup blob
|
||||||
|
*/
|
||||||
|
export async function uploadBackupBlob(blobJson: string, token: string): Promise<void> {
|
||||||
|
const payload: BackupBlob = { blob: blobJson }
|
||||||
|
|
||||||
|
const headers = getAuthHeaders(token, true);
|
||||||
|
const res = await fetch(`${API_BASE_URL}/crypto/backup`, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to upload backup blob");
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import { getAuthHeaders } from "./account";
|
||||||
|
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
||||||
|
import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
||||||
|
import { randomBytes } from "@/utils/crypto/kdf";
|
||||||
|
import { getCurrentKeys } from "./account";
|
||||||
|
import { request } from "@/core/websocket";
|
||||||
|
import type { SendDMRequest, DmEnvelope, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope, User } from "@/core/types";
|
||||||
|
import { b64, ub64 } from "@/utils/utils";
|
||||||
|
import { fetchUserPublicKey } from "./crypto";
|
||||||
|
import { fetchUsers, searchUsers } from "./users";
|
||||||
|
|
||||||
|
export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise<string> {
|
||||||
|
const keys = getCurrentKeys();
|
||||||
|
if (!keys) throw new Error("Keys not initialized");
|
||||||
|
|
||||||
|
// Obtain the key
|
||||||
|
const shared = ecdhSharedSecret(keys.privateKey, ub64(senderPublicKeyB64));
|
||||||
|
const wkRaw = await deriveWrappingKey(shared, ub64(envelope.salt), new Uint8Array([1]));
|
||||||
|
const wk = await importAesGcmKey(wkRaw);
|
||||||
|
const mk = await aesGcmDecrypt(wk, ub64(envelope.iv2), ub64(envelope.wrappedMk));
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
const msg = await aesGcmDecrypt(await importAesGcmKey(mk), ub64(envelope.iv), ub64(envelope.ciphertext));
|
||||||
|
return new TextDecoder().decode(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDMHistory(userId: number, token: string, limit: number = 50): Promise<DmEnvelope[]> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/dm/history/${userId}?limit=${limit}`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
if (!response.ok) return [];
|
||||||
|
const data = await response.json();
|
||||||
|
return data.messages || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export user functions for convenience
|
||||||
|
export { fetchUsers, searchUsers, fetchUserPublicKey };
|
||||||
|
|
||||||
|
export async function sendDMViaWebSocket(recipientId: number, recipientPublicKeyB64: string, plaintext: string, authToken: string, replyToId?: number): Promise<void> {
|
||||||
|
const keys = getCurrentKeys();
|
||||||
|
if (!keys) throw new Error("Keys not initialized");
|
||||||
|
|
||||||
|
// Encryption key
|
||||||
|
const mk = randomBytes(32);
|
||||||
|
const wkSalt = randomBytes(16);
|
||||||
|
const shared = ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64));
|
||||||
|
const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1]));
|
||||||
|
const wk = await importAesGcmKey(wkRaw);
|
||||||
|
|
||||||
|
// Encrypt the message
|
||||||
|
const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(plaintext));
|
||||||
|
const wrap = await aesGcmEncrypt(wk, mk);
|
||||||
|
|
||||||
|
const payload: SendDMRequest = {
|
||||||
|
recipientId: recipientId,
|
||||||
|
iv: b64(encMsg.iv),
|
||||||
|
ciphertext: b64(encMsg.ciphertext),
|
||||||
|
salt: b64(wkSalt),
|
||||||
|
iv2: b64(wrap.iv),
|
||||||
|
wrappedMk: b64(wrap.ciphertext)
|
||||||
|
};
|
||||||
|
if (replyToId) payload.replyToId = replyToId;
|
||||||
|
|
||||||
|
await request({
|
||||||
|
type: "dmSend",
|
||||||
|
credentials: {
|
||||||
|
scheme: "Bearer",
|
||||||
|
credentials: authToken
|
||||||
|
},
|
||||||
|
data: payload
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendDmWithFiles(recipientId: number, recipientPublicKeyB64: string, plaintextJson: string, files: File[], token: string): Promise<void> {
|
||||||
|
const keys = getCurrentKeys();
|
||||||
|
if (!keys) throw new Error("Keys not initialized");
|
||||||
|
|
||||||
|
const mk = randomBytes(32);
|
||||||
|
const wkSalt = randomBytes(16);
|
||||||
|
const shared = await ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64));
|
||||||
|
const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1]));
|
||||||
|
const wk = await importAesGcmKey(wkRaw);
|
||||||
|
|
||||||
|
const wrap = await aesGcmEncrypt(wk, mk);
|
||||||
|
|
||||||
|
const form = new FormData();
|
||||||
|
const names: string[] = [];
|
||||||
|
function sliceBuffer(u8: Uint8Array): ArrayBuffer {
|
||||||
|
return (u8.buffer as ArrayBuffer).slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const f of files) {
|
||||||
|
// Encrypt file with same mk
|
||||||
|
const data = new Uint8Array(await f.arrayBuffer());
|
||||||
|
const enc = await aesGcmEncrypt(await importAesGcmKey(mk), data);
|
||||||
|
const blob = new Blob([sliceBuffer(enc.iv), sliceBuffer(enc.ciphertext)], { type: "application/octet-stream" });
|
||||||
|
const serverName = f.name; // server uses provided name
|
||||||
|
names.push(serverName);
|
||||||
|
form.append("files", new File([blob], serverName));
|
||||||
|
}
|
||||||
|
form.append("fileNames", JSON.stringify(names));
|
||||||
|
|
||||||
|
// Merge files metadata into plaintext JSON and encrypt
|
||||||
|
let obj: DmEncryptedJSON;
|
||||||
|
try {
|
||||||
|
obj = JSON.parse(plaintextJson);
|
||||||
|
} catch {
|
||||||
|
obj = { type: "text", data: { content: String(plaintextJson) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(JSON.stringify(obj)));
|
||||||
|
form.append("dm_payload", JSON.stringify({
|
||||||
|
recipientId: recipientId,
|
||||||
|
iv: b64(encMsg.iv),
|
||||||
|
ciphertext: b64(encMsg.ciphertext),
|
||||||
|
salt: b64(wkSalt),
|
||||||
|
iv2: b64(wrap.iv),
|
||||||
|
wrappedMk: b64(wrap.ciphertext)
|
||||||
|
} satisfies BaseDmEnvelope));
|
||||||
|
|
||||||
|
await fetch(`${API_BASE_URL}/dm/send`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: getAuthHeaders(token, false),
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editDmEnvelope(id: number, recipientPublicKeyB64: string, newPlaintextJson: string, authToken: string): Promise<void> {
|
||||||
|
const keys = getCurrentKeys();
|
||||||
|
if (!keys) throw new Error("Keys not initialized");
|
||||||
|
|
||||||
|
// We cannot reuse the old mk safely without knowing it; generate a fresh mk and wrap
|
||||||
|
const mk = randomBytes(32);
|
||||||
|
const wkSalt = randomBytes(16);
|
||||||
|
const shared = await ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64));
|
||||||
|
const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1]));
|
||||||
|
const wk = await importAesGcmKey(wkRaw);
|
||||||
|
const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(newPlaintextJson));
|
||||||
|
const wrap = await aesGcmEncrypt(wk, mk);
|
||||||
|
|
||||||
|
await request({
|
||||||
|
type: "dmEdit",
|
||||||
|
credentials: { scheme: "Bearer", credentials: authToken },
|
||||||
|
data: {
|
||||||
|
id,
|
||||||
|
iv: b64(encMsg.iv),
|
||||||
|
ciphertext: b64(encMsg.ciphertext),
|
||||||
|
iv2: b64(wrap.iv),
|
||||||
|
wrappedMk: b64(wrap.ciphertext),
|
||||||
|
salt: b64(wkSalt)
|
||||||
|
}
|
||||||
|
} as DMEditRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteDmEnvelope(id: number, recipientId: number, authToken: string): Promise<void> {
|
||||||
|
await request({
|
||||||
|
type: "dmDelete",
|
||||||
|
credentials: { scheme: "Bearer", credentials: authToken },
|
||||||
|
data: { id, recipientId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DMConversationResponse {
|
||||||
|
user: User;
|
||||||
|
lastMessage: DmEnvelope;
|
||||||
|
unreadCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDMConversations(token: string): Promise<DMConversationResponse[]> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/dm/conversations`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
if (!res.ok) return [];
|
||||||
|
const data = await res.json();
|
||||||
|
return data.conversations || [];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { API_BASE_URL } from "@/core/config";
|
import { API_BASE_URL } from "@/core/config";
|
||||||
import { getAuthHeaders } from "./authApi";
|
import { getAuthHeaders } from "./account";
|
||||||
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
||||||
import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
||||||
import { randomBytes } from "@/utils/crypto/kdf";
|
import { randomBytes } from "@/utils/crypto/kdf";
|
||||||
import { getCurrentKeys } from "./authApi";
|
import { getCurrentKeys } from "./account";
|
||||||
import { request } from "@/core/websocket";
|
import { request } from "@/core/websocket";
|
||||||
import type { SendDMRequest, DmEnvelope, User, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope } from "@/core/types";
|
import type { SendDMRequest, DmEnvelope, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope, User } from "@/core/types";
|
||||||
import { b64, ub64 } from "@/utils/utils";
|
import { b64, ub64 } from "@/utils/utils";
|
||||||
|
import { fetchUserPublicKey } from "./crypto";
|
||||||
|
import { fetchUsers, searchUsers } from "./users";
|
||||||
|
|
||||||
export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise<string> {
|
export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise<string> {
|
||||||
const keys = getCurrentKeys();
|
const keys = getCurrentKeys();
|
||||||
@@ -23,20 +25,6 @@ export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string
|
|||||||
return new TextDecoder().decode(msg);
|
return new TextDecoder().decode(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchUsers(token: string): Promise<User[]> {
|
|
||||||
const res = await fetch(`${API_BASE_URL}/users`, { headers: getAuthHeaders(token, true) });
|
|
||||||
if (!res.ok) return [];
|
|
||||||
const data = await res.json();
|
|
||||||
return data.users || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchUserPublicKey(userId: number, token: string): Promise<string | null> {
|
|
||||||
const res = await fetch(`${API_BASE_URL}/crypto/public-key/of/${userId}`, { headers: getAuthHeaders(token, true) });
|
|
||||||
if (!res.ok) return null;
|
|
||||||
const data = await res.json();
|
|
||||||
return data.publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchDMHistory(userId: number, token: string, limit: number = 50): Promise<DmEnvelope[]> {
|
export async function fetchDMHistory(userId: number, token: string, limit: number = 50): Promise<DmEnvelope[]> {
|
||||||
const response = await fetch(`${API_BASE_URL}/dm/history/${userId}?limit=${limit}`, {
|
const response = await fetch(`${API_BASE_URL}/dm/history/${userId}?limit=${limit}`, {
|
||||||
headers: getAuthHeaders(token, true)
|
headers: getAuthHeaders(token, true)
|
||||||
@@ -46,6 +34,9 @@ export async function fetchDMHistory(userId: number, token: string, limit: numbe
|
|||||||
return data.messages || [];
|
return data.messages || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-export user functions for convenience
|
||||||
|
export { fetchUsers, searchUsers, fetchUserPublicKey };
|
||||||
|
|
||||||
export async function sendDMViaWebSocket(recipientId: number, recipientPublicKeyB64: string, plaintext: string, authToken: string, replyToId?: number): Promise<void> {
|
export async function sendDMViaWebSocket(recipientId: number, recipientPublicKeyB64: string, plaintext: string, authToken: string, replyToId?: number): Promise<void> {
|
||||||
const keys = getCurrentKeys();
|
const keys = getCurrentKeys();
|
||||||
if (!keys) throw new Error("Keys not initialized");
|
if (!keys) throw new Error("Keys not initialized");
|
||||||
@@ -185,13 +176,3 @@ export async function fetchDMConversations(token: string): Promise<DMConversatio
|
|||||||
return data.conversations || [];
|
return data.conversations || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchUsers(query: string, token: string): Promise<User[]> {
|
|
||||||
if (query.length < 2) return [];
|
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_URL}/users/search?q=${encodeURIComponent(query)}`, {
|
|
||||||
headers: getAuthHeaders(token, true)
|
|
||||||
});
|
|
||||||
if (!res.ok) return [];
|
|
||||||
const data = await res.json();
|
|
||||||
return data.users || [];
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import { getAuthHeaders } from "./account";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the URL for a normal (unencrypted) file
|
||||||
|
*/
|
||||||
|
export function getNormalFileUrl(filename: string): string {
|
||||||
|
return `${API_BASE_URL}/uploads/files/normal/${filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the URL for an encrypted file
|
||||||
|
*/
|
||||||
|
export function getEncryptedFileUrl(filename: string): string {
|
||||||
|
return `${API_BASE_URL}/uploads/files/encrypted/${filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a normal file (unencrypted)
|
||||||
|
*/
|
||||||
|
export async function fetchNormalFile(filename: string, token: string): Promise<Blob> {
|
||||||
|
const res = await fetch(getNormalFileUrl(filename), {
|
||||||
|
headers: getAuthHeaders(token, false)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch file");
|
||||||
|
return await res.blob();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches an encrypted file
|
||||||
|
*/
|
||||||
|
export async function fetchEncryptedFile(filename: string, token: string): Promise<Blob> {
|
||||||
|
const res = await fetch(getEncryptedFileUrl(filename), {
|
||||||
|
headers: getAuthHeaders(token, false)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch encrypted file");
|
||||||
|
return await res.blob();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import { getAuthHeaders } from "./account";
|
||||||
|
import type { Message, Messages, SendMessageRequest } from "@/core/types";
|
||||||
|
import { request } from "@/core/websocket";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches public chat messages
|
||||||
|
*/
|
||||||
|
export async function fetchMessages(token: string, limit: number = 50, beforeId?: number): Promise<Message[]> {
|
||||||
|
let url = `${API_BASE_URL}/get_messages?limit=${limit}`;
|
||||||
|
if (beforeId) {
|
||||||
|
url += `&before_id=${beforeId}`;
|
||||||
|
}
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
if (!response.ok) return [];
|
||||||
|
const data: Messages = await response.json();
|
||||||
|
return data.messages || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a public chat message via WebSocket
|
||||||
|
*/
|
||||||
|
export async function sendMessage(content: string, replyToId: number | null, authToken: string): Promise<void> {
|
||||||
|
await request({
|
||||||
|
data: {
|
||||||
|
content: content.trim(),
|
||||||
|
reply_to_id: replyToId ?? null
|
||||||
|
},
|
||||||
|
credentials: {
|
||||||
|
scheme: "Bearer",
|
||||||
|
credentials: authToken
|
||||||
|
},
|
||||||
|
type: "sendMessage"
|
||||||
|
} satisfies SendMessageRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a public chat message with files via HTTP
|
||||||
|
*/
|
||||||
|
export async function sendMessageWithFiles(
|
||||||
|
content: string,
|
||||||
|
replyToId: number | null,
|
||||||
|
files: File[],
|
||||||
|
authToken: string
|
||||||
|
): Promise<void> {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("payload", JSON.stringify({
|
||||||
|
content: content.trim(),
|
||||||
|
reply_to_id: replyToId ?? null
|
||||||
|
} satisfies SendMessageRequest["data"]));
|
||||||
|
for (const f of files) form.append("files", f, f.name);
|
||||||
|
const res = await fetch(`${API_BASE_URL}/send_message`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: getAuthHeaders(authToken, false),
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.text();
|
||||||
|
throw new Error(error || "Failed to send message with files");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits a public chat message
|
||||||
|
*/
|
||||||
|
export async function editMessage(messageId: number, newContent: string, authToken: string): Promise<void> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/edit_message/${messageId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: getAuthHeaders(authToken, true),
|
||||||
|
body: JSON.stringify({ content: newContent })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to edit message");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a public chat message
|
||||||
|
*/
|
||||||
|
export async function deleteMessage(messageId: number, authToken: string): Promise<void> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/delete_message/${messageId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: getAuthHeaders(authToken, true)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to delete message");
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import { getAuthHeaders } from "./account";
|
||||||
|
|
||||||
|
export interface BlocklistResponse {
|
||||||
|
words: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlocklistUpdateRequest {
|
||||||
|
words: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlocklistUpdateResponse {
|
||||||
|
added?: string[];
|
||||||
|
removed?: string[];
|
||||||
|
words: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the current blocklist (admin only)
|
||||||
|
*/
|
||||||
|
export async function getBlocklist(token: string): Promise<BlocklistResponse> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/moderation/blocklist`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch blocklist");
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds words to the blocklist (admin only)
|
||||||
|
*/
|
||||||
|
export async function addToBlocklist(words: string[], token: string): Promise<BlocklistUpdateResponse> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/moderation/blocklist`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: getAuthHeaders(token, true),
|
||||||
|
body: JSON.stringify({ words })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to add to blocklist");
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes words from the blocklist (admin only)
|
||||||
|
*/
|
||||||
|
export async function removeFromBlocklist(words: string[], token: string): Promise<BlocklistUpdateResponse> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/moderation/blocklist`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: getAuthHeaders(token, true),
|
||||||
|
body: JSON.stringify({ words })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to remove from blocklist");
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAuthHeaders } from "./authApi";
|
import { getAuthHeaders } from "./account";
|
||||||
import { API_BASE_URL } from "@/core/config";
|
import { API_BASE_URL } from "@/core/config";
|
||||||
import type { UserProfile } from "@/core/types";
|
import type { UserProfile } from "@/core/types";
|
||||||
|
|
||||||
@@ -208,3 +208,67 @@ export async function checkUserSimilarity(userId: number, token: string): Promis
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suspends a user account (admin only)
|
||||||
|
*/
|
||||||
|
export async function suspendUser(userId: number, reason: string, token: string): Promise<{status: string; message: string; reason: string} | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/${userId}/suspend`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(token),
|
||||||
|
body: JSON.stringify({ reason })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error suspending user:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsuspends a user account (admin only)
|
||||||
|
*/
|
||||||
|
export async function unsuspendUser(userId: number, token: string): Promise<{status: string; message: string} | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/${userId}/unsuspend`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(token)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unsuspending user:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a user account (admin only)
|
||||||
|
*/
|
||||||
|
export async function deleteUser(userId: number, token: string): Promise<{status: string; message: string} | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/user/${userId}/delete`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(token)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting user:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import { getAuthHeaders } from "./account";
|
||||||
|
|
||||||
|
export interface PushSubscriptionRequest {
|
||||||
|
endpoint: string;
|
||||||
|
keys: {
|
||||||
|
p256dh: string;
|
||||||
|
auth: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PushSubscriptionResponse {
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes the current user to push notifications
|
||||||
|
*/
|
||||||
|
export async function subscribeToPush(
|
||||||
|
subscription: PushSubscriptionRequest,
|
||||||
|
token: string
|
||||||
|
): Promise<PushSubscriptionResponse> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/push/subscribe`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: getAuthHeaders(token, true),
|
||||||
|
body: JSON.stringify(subscription)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ detail: "Failed to subscribe to push notifications" }));
|
||||||
|
throw new Error(error.detail || "Failed to subscribe to push notifications");
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribes the current user from push notifications
|
||||||
|
*/
|
||||||
|
export async function unsubscribeFromPush(token: string): Promise<PushSubscriptionResponse> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/push/unsubscribe`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ detail: "Failed to unsubscribe from push notifications" }));
|
||||||
|
throw new Error(error.detail || "Failed to unsubscribe from push notifications");
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { API_BASE_URL } from "@/core/config";
|
|
||||||
import { getAuthHeaders, deriveAuthSecret } from "@/core/api/authApi";
|
|
||||||
|
|
||||||
export async function changePassword(
|
|
||||||
token: string,
|
|
||||||
username: string,
|
|
||||||
currentPassword: string,
|
|
||||||
newPassword: string,
|
|
||||||
logoutAllExceptCurrent: boolean
|
|
||||||
): Promise<void> {
|
|
||||||
const currentDerived = await deriveAuthSecret(username, currentPassword);
|
|
||||||
const newDerived = await deriveAuthSecret(username, newPassword);
|
|
||||||
const res = await fetch(`${API_BASE_URL}/change-password`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: getAuthHeaders(token),
|
|
||||||
body: JSON.stringify({
|
|
||||||
currentPasswordDerived: currentDerived,
|
|
||||||
newPasswordDerived: newDerived,
|
|
||||||
logoutAllExceptCurrent
|
|
||||||
})
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error("Failed to change password");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteAccount(token: string): Promise<void> {
|
|
||||||
const res = await fetch(`${API_BASE_URL}/account/delete`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: getAuthHeaders(token)
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const error = await res.json().catch(() => ({ detail: "Failed to delete account" }));
|
|
||||||
throw new Error(error.detail || "Failed to delete account");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import { getAuthHeaders } from "./account";
|
||||||
|
import type { User } from "@/core/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a list of all users (excluding current user)
|
||||||
|
*/
|
||||||
|
export async function fetchUsers(token: string): Promise<User[]> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/users`, { headers: getAuthHeaders(token, true) });
|
||||||
|
if (!res.ok) return [];
|
||||||
|
const data = await res.json();
|
||||||
|
return data.users || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for users by username query
|
||||||
|
*/
|
||||||
|
export async function searchUsers(query: string, token: string): Promise<User[]> {
|
||||||
|
if (query.length < 2) return [];
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE_URL}/users/search?q=${encodeURIComponent(query)}`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
if (!res.ok) return [];
|
||||||
|
const data = await res.json();
|
||||||
|
return data.users || [];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import { getAuthHeaders } from "./account";
|
||||||
|
import type { IceServersResponse } from "@/core/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches ICE server configuration for WebRTC
|
||||||
|
*/
|
||||||
|
export async function getIceServers(token: string): Promise<IceServersResponse> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/webrtc/ice`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch ICE servers");
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "@/utils/crypto/sy
|
|||||||
import { randomBytes } from "@/utils/crypto/kdf";
|
import { randomBytes } from "@/utils/crypto/kdf";
|
||||||
import { b64, ub64 } from "@/utils/utils";
|
import { b64, ub64 } from "@/utils/utils";
|
||||||
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
||||||
import { getCurrentKeys } from "@/core/api/authApi";
|
import { getCurrentKeys } from "@/core/api/account";
|
||||||
import type { WrappedSessionKeyPayload } from "@/core/types";
|
import type { WrappedSessionKeyPayload } from "@/core/types";
|
||||||
|
|
||||||
export interface CallSessionKey {
|
export interface CallSessionKey {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { getAuthHeaders, getAuthToken } from "@/core/api/authApi";
|
import { getAuthToken } from "@/core/api/account";
|
||||||
import type { CallSignalingMessage, IceServersResponse, WrappedSessionKeyPayload } from "@/core/types";
|
import type { CallSignalingMessage, WrappedSessionKeyPayload } from "@/core/types";
|
||||||
|
import { getIceServers as fetchIceServers } from "@/core/api/webrtc";
|
||||||
import { request } from "@/core/websocket";
|
import { request } from "@/core/websocket";
|
||||||
import { wrapCallSessionKeyForRecipient, unwrapCallSessionKeyFromSender, rotateCallSessionKey } from "./encryption";
|
import { wrapCallSessionKeyForRecipient, unwrapCallSessionKeyFromSender, rotateCallSessionKey } from "./encryption";
|
||||||
import { fetchUserPublicKey } from "@/core/api/dmApi";
|
import { fetchUserPublicKey } from "@/core/api/dm";
|
||||||
import { importAesGcmKey } from "@/utils/crypto/symmetric";
|
import { importAesGcmKey } from "@/utils/crypto/symmetric";
|
||||||
import E2EEWorker from "./e2eeWorker?worker";
|
import E2EEWorker from "./e2eeWorker?worker";
|
||||||
import { delay } from "@/utils/utils";
|
import { delay } from "@/utils/utils";
|
||||||
@@ -99,16 +100,10 @@ export class WebRTCCall {
|
|||||||
*/
|
*/
|
||||||
private async getIceServers(): Promise<RTCIceServer[]> {
|
private async getIceServers(): Promise<RTCIceServer[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/webrtc/ice", {
|
const token = getAuthToken();
|
||||||
headers: getAuthHeaders(getAuthToken()!)
|
if (!token) throw new Error("No auth token");
|
||||||
});
|
const data = await fetchIceServers(token);
|
||||||
|
return data.iceServers || [];
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json() as IceServersResponse;
|
|
||||||
return data.iceServers || [];
|
|
||||||
} else {
|
|
||||||
console.warn("Failed to fetch ICE servers:", response.status, response.statusText);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to fetch ICE servers:", error);
|
console.warn("Failed to fetch ICE servers:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { checkUserSimilarity } from "@/core/api/profileApi";
|
import { checkUserSimilarity } from "@/core/api/account/profile";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { MaterialIcon } from "@/utils/material";
|
import { MaterialIcon } from "@/utils/material";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { verifyUser } from "@/core/api/profileApi";
|
import { verifyUser } from "@/core/api/account/profile";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { MaterialButton } from "@/utils/material";
|
import { MaterialButton } from "@/utils/material";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { API_BASE_URL } from "@/core/config";
|
import { subscribeToPush } from "@/core/api/push";
|
||||||
import { isElectron } from "@/core/electron/electron";
|
import { isElectron } from "@/core/electron/electron";
|
||||||
import { websocket } from "@/core/websocket";
|
import { websocket } from "@/core/websocket";
|
||||||
import type { NewMessageWebSocketMessage, WebSocketMessage } from "@/core/types";
|
import type { NewMessageWebSocketMessage, WebSocketMessage } from "@/core/types";
|
||||||
@@ -89,16 +89,8 @@ async function sendSubscriptionToServer(token: string): Promise<boolean> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/push/subscribe`, {
|
await subscribeToPush(subscriptionData, token);
|
||||||
method: "POST",
|
return true;
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Bearer ${token}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify(subscriptionData)
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.ok;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send subscription to server:", error);
|
console.error("Failed to send subscription to server:", error);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { useRef, useState } from "react";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { motion, type Transition, type Variants } from "motion/react";
|
import { motion, type Transition, type Variants } from "motion/react";
|
||||||
import { useImmer } from "use-immer";
|
import { useImmer } from "use-immer";
|
||||||
import type { ErrorResponse, LoginRequest, LoginResponse } from "@/core/types";
|
import type { LoginRequest } from "@/core/types";
|
||||||
import { API_BASE_URL } from "@/core/config";
|
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { MaterialButton } from "@/utils/material";
|
import { MaterialButton } from "@/utils/material";
|
||||||
import { ensureKeysOnLogin, deriveAuthSecret } from "@/core/api/authApi";
|
import { ensureKeysOnLogin, deriveAuthSecret, login } from "@/core/api/account";
|
||||||
import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
|
import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
|
||||||
import { initialize, isSupported, startElectronReceiver, subscribe } from "@/core/push-notifications/push-notifications";
|
import { initialize, isSupported, startElectronReceiver, subscribe } from "@/core/push-notifications/push-notifications";
|
||||||
import { isElectron } from "@/core/electron/electron";
|
import { isElectron } from "@/core/electron/electron";
|
||||||
@@ -86,16 +85,8 @@ export function LoginForm({ onSwitchMode }: LoginFormProps) {
|
|||||||
password: derived
|
password: derived
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}/login`, {
|
try {
|
||||||
method: 'POST',
|
const data = await login(request);
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(request)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data: LoginResponse = await response.json();
|
|
||||||
setUser(data.token, data.user);
|
setUser(data.token, data.user);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -126,20 +117,16 @@ export function LoginForm({ onSwitchMode }: LoginFormProps) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Notification setup failed:", e);
|
console.error("Notification setup failed:", e);
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error: any) {
|
||||||
const data: ErrorResponse = await response.json();
|
if (error.message && error.message.includes("suspension")) {
|
||||||
|
|
||||||
if (response.status === 403 && response.headers.get("suspension_reason")) {
|
|
||||||
const suspensionReason = response.headers.get("suspension_reason");
|
|
||||||
const setSuspended = useAppState.getState().setSuspended;
|
const setSuspended = useAppState.getState().setSuspended;
|
||||||
setSuspended(suspensionReason || "No reason provided");
|
setSuspended(error.message || "No reason provided");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
showAlert("danger", error.message || "Неверное имя пользователя или пароль");
|
||||||
showAlert("danger", data.message || "Неверное имя пользователя или пароль");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
showAlert("danger", "Ошибка соединения с сервером");
|
showAlert("danger", error.message || "Ошибка соединения с сервером");
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { useRef, useState } from "react";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { motion, type Transition, type Variants } from "motion/react";
|
import { motion, type Transition, type Variants } from "motion/react";
|
||||||
import { useImmer } from "use-immer";
|
import { useImmer } from "use-immer";
|
||||||
import type { ErrorResponse, RegisterRequest, LoginResponse } from "@/core/types";
|
import type { RegisterRequest } from "@/core/types";
|
||||||
import { API_BASE_URL } from "@/core/config";
|
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { MaterialButton, MaterialIconButton } from "@/utils/material";
|
import { MaterialButton, MaterialIconButton } from "@/utils/material";
|
||||||
import { ensureKeysOnLogin, deriveAuthSecret } from "@/core/api/authApi";
|
import { ensureKeysOnLogin, deriveAuthSecret, register } from "@/core/api/account";
|
||||||
import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
|
import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
|
||||||
import type { Alert, AlertType } from "./Auth";
|
import type { Alert, AlertType } from "./Auth";
|
||||||
import { AuthHeader, AlertsContainer } from "./Auth";
|
import { AuthHeader, AlertsContainer } from "./Auth";
|
||||||
@@ -115,16 +114,8 @@ export function RegisterForm({ onSwitchMode }: RegisterFormProps) {
|
|||||||
confirm_password: derived
|
confirm_password: derived
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}/register`, {
|
try {
|
||||||
method: 'POST',
|
const data = await register(request);
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(request)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data: LoginResponse = await response.json();
|
|
||||||
setUser(data.token, data.user);
|
setUser(data.token, data.user);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -134,12 +125,11 @@ export function RegisterForm({ onSwitchMode }: RegisterFormProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
navigate("/chat");
|
navigate("/chat");
|
||||||
} else {
|
} catch (error: any) {
|
||||||
const data: ErrorResponse = await response.json();
|
showAlert("danger", error.message || "Ошибка при регистрации");
|
||||||
showAlert("danger", data.message || "Ошибка при регистрации");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
showAlert("danger", "Ошибка соединения с сервером");
|
showAlert("danger", error.message || "Ошибка соединения с сервером");
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
sendDMViaWebSocket,
|
sendDMViaWebSocket,
|
||||||
fetchDMConversations,
|
fetchDMConversations,
|
||||||
type DMConversationResponse
|
type DMConversationResponse
|
||||||
} from "@/core/api/dmApi";
|
} from "@/core/api/dm";
|
||||||
import type { User, Message, DmEncryptedJSON } from "@/core/types";
|
import type { User, Message, DmEncryptedJSON } from "@/core/types";
|
||||||
import { websocket } from "@/core/websocket";
|
import { websocket } from "@/core/websocket";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect } from "react";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "@/core/api/profileApi";
|
import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "@/core/api/account/profile";
|
||||||
import { showSuccess, showError } from "@/utils/notification";
|
import { showSuccess, showError } from "@/utils/notification";
|
||||||
|
|
||||||
export default function useProfile() {
|
export default function useProfile() {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { request } from "@/core/websocket";
|
|||||||
import { MessagePanel } from "./ui/right/panels/MessagePanel";
|
import { MessagePanel } from "./ui/right/panels/MessagePanel";
|
||||||
import { PublicChatPanel } from "./ui/right/panels/PublicChatPanel";
|
import { PublicChatPanel } from "./ui/right/panels/PublicChatPanel";
|
||||||
import { DMPanel, type DMPanelData } from "./ui/right/panels/DMPanel";
|
import { DMPanel, type DMPanelData } from "./ui/right/panels/DMPanel";
|
||||||
import { getAuthHeaders } from "@/core/api/authApi";
|
import { restoreKeys } from "@/core/api/account";
|
||||||
import { restoreKeys } from "@/core/api/authApi";
|
|
||||||
import { API_BASE_URL } from "@/core/config";
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import { getAuthHeaders } from "@/core/api/account";
|
||||||
import { initialize, subscribe, startElectronReceiver, isSupported } from "@/core/push-notifications/push-notifications";
|
import { initialize, subscribe, startElectronReceiver, isSupported } from "@/core/push-notifications/push-notifications";
|
||||||
import { isElectron } from "@/core/electron/electron";
|
import { isElectron } from "@/core/electron/electron";
|
||||||
import { onlineStatusManager } from "@/core/onlineStatusManager";
|
import { onlineStatusManager } from "@/core/onlineStatusManager";
|
||||||
@@ -298,12 +298,12 @@ export const useAppState = create<AppState>((set, get) => ({
|
|||||||
const token = localStorage.getItem('authToken');
|
const token = localStorage.getItem('authToken');
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
const response = await fetch(`${API_BASE_URL}/user/profile`, {
|
// Fetch full user profile
|
||||||
headers: getAuthHeaders(token)
|
const fullResponse = await fetch(`${API_BASE_URL}/user/profile`, {
|
||||||
|
headers: getAuthHeaders(token, true)
|
||||||
});
|
});
|
||||||
|
if (fullResponse.ok) {
|
||||||
if (response.ok) {
|
const user: User = await fullResponse.json();
|
||||||
const user: User = await response.json();
|
|
||||||
restoreKeys();
|
restoreKeys();
|
||||||
|
|
||||||
// Check if user is suspended
|
// Check if user is suspended
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { CallWindow } from "./right/calls/CallWindow";
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { fetchUserProfileById, fetchUserProfile } from "@/core/api/profileApi";
|
import { fetchUserProfileById, fetchUserProfile } from "@/core/api/account/profile";
|
||||||
import styles from "@/pages/chat/css/layout.module.scss";
|
import styles from "@/pages/chat/css/layout.module.scss";
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { ProfileDialogData } from "@/pages/chat/state";
|
|||||||
import defaultAvatar from "@/images/default-avatar.png";
|
import defaultAvatar from "@/images/default-avatar.png";
|
||||||
import { confirm } from "mdui/functions/confirm";
|
import { confirm } from "mdui/functions/confirm";
|
||||||
import { prompt } from "mdui/functions/prompt";
|
import { prompt } from "mdui/functions/prompt";
|
||||||
import { updateProfile, uploadProfilePicture, fetchUserProfileById } from "@/core/api/profileApi";
|
import { updateProfile, uploadProfilePicture, fetchUserProfileById, suspendUser, unsuspendUser, deleteUser } from "@/core/api/account/profile";
|
||||||
import { RichTextArea } from "@/core/components/RichTextArea";
|
import { RichTextArea } from "@/core/components/RichTextArea";
|
||||||
import { StatusBadge } from "@/core/components/StatusBadge";
|
import { StatusBadge } from "@/core/components/StatusBadge";
|
||||||
import { VerifyButton } from "@/core/components/VerifyButton";
|
import { VerifyButton } from "@/core/components/VerifyButton";
|
||||||
@@ -349,37 +349,20 @@ export function ProfileDialog() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (reason) {
|
if (reason) {
|
||||||
const response = await fetch(`/api/user/${currentData.userId}/suspend`, {
|
const result = await suspendUser(currentData.userId, reason, user.authToken!);
|
||||||
method: "POST",
|
if (result) {
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${user.authToken}`,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ reason })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
closeProfileDialog();
|
closeProfileDialog();
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
console.error("Failed to suspend user");
|
||||||
console.error("Failed to suspend user:", error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unsuspend user
|
// Unsuspend user
|
||||||
const response = await fetch(`/api/user/${currentData.userId}/unsuspend`, {
|
const result = await unsuspendUser(currentData.userId, user.authToken!);
|
||||||
method: "POST",
|
if (result) {
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${user.authToken}`,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
closeProfileDialog();
|
closeProfileDialog();
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
console.error("Failed to unsuspend user");
|
||||||
console.error("Failed to unsuspend user:", error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -398,19 +381,12 @@ export function ProfileDialog() {
|
|||||||
cancelText: "Cancel"
|
cancelText: "Cancel"
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(`/api/user/${currentData.userId}/delete`, {
|
const result = await deleteUser(currentData.userId, user.authToken!);
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${user.authToken}`,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (result) {
|
||||||
closeProfileDialog();
|
closeProfileDialog();
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
console.error("Failed to delete user");
|
||||||
console.error("Failed to delete user:", error);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// User cancelled or error occurred
|
// User cancelled or error occurred
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { useDM, type DMUser } from "@/pages/chat/hooks/useDM";
|
import { useDM, type DMUser } from "@/pages/chat/hooks/useDM";
|
||||||
import { API_BASE_URL } from "@/core/config";
|
import { fetchMessages } from "@/core/api/messaging";
|
||||||
import { getAuthHeaders } from "@/core/api/authApi";
|
import { fetchUserPublicKey } from "@/core/api/dm";
|
||||||
import { fetchUserPublicKey } from "@/core/api/dmApi";
|
|
||||||
import { StatusBadge } from "@/core/components/StatusBadge";
|
import { StatusBadge } from "@/core/components/StatusBadge";
|
||||||
import type { Message } from "@/core/types";
|
import type { Message } from "@/core/types";
|
||||||
import { websocket } from "@/core/websocket";
|
import { websocket } from "@/core/websocket";
|
||||||
@@ -51,16 +50,10 @@ export function UnifiedChatsList() {
|
|||||||
if (!user.authToken) return;
|
if (!user.authToken) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/get_messages`, {
|
const messages = await fetchMessages(user.authToken, 1);
|
||||||
headers: getAuthHeaders(user.authToken)
|
if (messages?.length > 0) {
|
||||||
});
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
setLastMessages({ general: lastMessage });
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.messages?.length > 0) {
|
|
||||||
const lastMessage = data.messages[data.messages.length - 1];
|
|
||||||
setLastMessages({ general: lastMessage });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading last messages:", error);
|
console.error("Error loading last messages:", error);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { searchUsers, fetchUserPublicKey } from "@/core/api/dmApi";
|
import { searchUsers, fetchUserPublicKey } from "@/core/api/dm";
|
||||||
import { StatusBadge } from "@/core/components/StatusBadge";
|
import { StatusBadge } from "@/core/components/StatusBadge";
|
||||||
import type { User } from "@/core/types";
|
import type { User } from "@/core/types";
|
||||||
import { onlineStatusManager } from "@/core/onlineStatusManager";
|
import { onlineStatusManager } from "@/core/onlineStatusManager";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MaterialList, MaterialListItem } from "@/utils/material";
|
import { MaterialList, MaterialListItem } from "@/utils/material";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { deleteAccount } from "@/core/api/securityApi";
|
import { deleteAccount } from "@/core/api/account";
|
||||||
import { confirm } from "mdui/functions/confirm";
|
import { confirm } from "mdui/functions/confirm";
|
||||||
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
|||||||
import { StyledDialog } from "@/core/components/StyledDialog";
|
import { StyledDialog } from "@/core/components/StyledDialog";
|
||||||
import type { DialogProps } from "@/core/types";
|
import type { DialogProps } from "@/core/types";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { changePassword } from "@/core/api/securityApi";
|
import { changePassword } from "@/core/api/account";
|
||||||
import { MaterialButton, MaterialIconButton, MaterialSwitch, MaterialTextField } from "@/utils/material";
|
import { MaterialButton, MaterialIconButton, MaterialSwitch, MaterialTextField } from "@/utils/material";
|
||||||
import styles from "@/pages/chat/css/changePasswordDialog.module.scss";
|
import styles from "@/pages/chat/css/changePasswordDialog.module.scss";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
|
|||||||
import { useImmer } from "use-immer";
|
import { useImmer } from "use-immer";
|
||||||
import { MaterialList, MaterialListItem, MaterialButton, MaterialCircularProgress } from "@/utils/material";
|
import { MaterialList, MaterialListItem, MaterialButton, MaterialCircularProgress } from "@/utils/material";
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { listDevices, revokeDevice, logoutAllOtherDevices, type DeviceInfo } from "@/core/api/devicesApi";
|
import { listDevices, revokeDevice, logoutAllOtherDevices, type DeviceInfo } from "@/core/api/account/devices";
|
||||||
import { confirm } from "mdui/functions/confirm";
|
import { confirm } from "mdui/functions/confirm";
|
||||||
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { MaterialList, MaterialListItem, MaterialSwitch, type MDUISwitch } from
|
|||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { initialize, subscribe, unsubscribe, isSupported } from "@/core/push-notifications/push-notifications";
|
import { initialize, subscribe, unsubscribe, isSupported } from "@/core/push-notifications/push-notifications";
|
||||||
import { isElectron } from "@/core/electron/electron";
|
import { isElectron } from "@/core/electron/electron";
|
||||||
import { API_BASE_URL } from "@/core/config";
|
import { unsubscribeFromPush } from "@/core/api/push";
|
||||||
import { getAuthHeaders } from "@/core/api/authApi";
|
|
||||||
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
||||||
|
|
||||||
export function NotificationsPanel() {
|
export function NotificationsPanel() {
|
||||||
@@ -74,14 +73,7 @@ export function NotificationsPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Then unsubscribe from server
|
// Then unsubscribe from server
|
||||||
const response = await fetch(`${API_BASE_URL}/push/unsubscribe`, {
|
await unsubscribeFromPush(authToken);
|
||||||
method: "DELETE",
|
|
||||||
headers: getAuthHeaders(authToken)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to unsubscribe from push notifications");
|
|
||||||
}
|
|
||||||
|
|
||||||
// After unsubscribing, permission is still granted but we're not subscribed
|
// After unsubscribing, permission is still granted but we're not subscribed
|
||||||
// So we keep the state as disabled (false)
|
// So we keep the state as disabled (false)
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import Quote from "@/core/components/Quote";
|
|||||||
import { parse } from "marked";
|
import { parse } from "marked";
|
||||||
import { escape as escapeHtml } from "he";
|
import { escape as escapeHtml } from "he";
|
||||||
import { useEffect, useState, useRef, useMemo } from "react";
|
import { useEffect, useState, useRef, useMemo } from "react";
|
||||||
import { getCurrentKeys } from "@/core/api/authApi";
|
import { getCurrentKeys, getAuthHeaders } from "@/core/api/account";
|
||||||
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
||||||
import { importAesGcmKey, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
import { importAesGcmKey, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
||||||
import { getAuthHeaders } from "@/core/api/authApi";
|
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import { fetchUserProfileById, fetchUserProfile } from "@/core/api/profileApi";
|
import { fetchUserProfileById, fetchUserProfile } from "@/core/api/account/profile";
|
||||||
import { StatusBadge } from "@/core/components/StatusBadge";
|
import { StatusBadge } from "@/core/components/StatusBadge";
|
||||||
import { ub64 } from "@/utils/utils";
|
import { ub64 } from "@/utils/utils";
|
||||||
import { useImmer } from "use-immer";
|
import { useImmer } from "use-immer";
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
sendDmWithFiles,
|
sendDmWithFiles,
|
||||||
editDmEnvelope,
|
editDmEnvelope,
|
||||||
deleteDmEnvelope
|
deleteDmEnvelope
|
||||||
} from "@/core/api/dmApi";
|
} from "@/core/api/dm";
|
||||||
import { fetchUserProfileById } from "@/core/api/profileApi";
|
import { fetchUserProfileById } from "@/core/api/account/profile";
|
||||||
import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "@/core/types";
|
import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "@/core/types";
|
||||||
import type { UserState, ProfileDialogData } from "@/pages/chat/state";
|
import type { UserState, ProfileDialogData } from "@/pages/chat/state";
|
||||||
import { formatDMUsername } from "@/pages/chat/hooks/useDM";
|
import { formatDMUsername } from "@/pages/chat/hooks/useDM";
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { MessagePanel } from "./MessagePanel";
|
import { MessagePanel } from "./MessagePanel";
|
||||||
import { API_BASE_URL } from "@/core/config";
|
|
||||||
import { getAuthHeaders } from "@/core/api/authApi";
|
|
||||||
import { request } from "@/core/websocket";
|
import { request } from "@/core/websocket";
|
||||||
import type { ChatWebSocketMessage, Message, SendMessageRequest, ReactionUpdateWebSocketMessage } from "@/core/types";
|
import type { ChatWebSocketMessage, Message, ReactionUpdateWebSocketMessage } from "@/core/types";
|
||||||
import type { UserState, ProfileDialogData } from "@/pages/chat/state";
|
import type { UserState, ProfileDialogData } from "@/pages/chat/state";
|
||||||
|
import { fetchMessages, sendMessage, sendMessageWithFiles } from "@/core/api/messaging";
|
||||||
|
|
||||||
export class PublicChatPanel extends MessagePanel {
|
export class PublicChatPanel extends MessagePanel {
|
||||||
private messagesLoaded: boolean = false;
|
private messagesLoaded: boolean = false;
|
||||||
@@ -42,18 +41,12 @@ export class PublicChatPanel extends MessagePanel {
|
|||||||
|
|
||||||
this.setLoading(true);
|
this.setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/get_messages`, {
|
const messages = await fetchMessages(this.currentUser.authToken);
|
||||||
headers: getAuthHeaders(this.currentUser.authToken)
|
if (messages && messages.length > 0) {
|
||||||
});
|
this.clearMessages();
|
||||||
|
messages.forEach((msg: Message) => {
|
||||||
if (response.ok) {
|
this.addMessage(msg);
|
||||||
const data = await response.json();
|
});
|
||||||
if (data.messages && data.messages.length > 0) {
|
|
||||||
this.clearMessages();
|
|
||||||
data.messages.forEach((msg: Message) => {
|
|
||||||
this.addMessage(msg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.messagesLoaded = true;
|
this.messagesLoaded = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -68,35 +61,9 @@ export class PublicChatPanel extends MessagePanel {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
const response = await request({
|
await sendMessage(content, replyToId ?? null, this.currentUser.authToken);
|
||||||
data: {
|
|
||||||
content: content.trim(),
|
|
||||||
reply_to_id: replyToId ?? null
|
|
||||||
},
|
|
||||||
credentials: {
|
|
||||||
scheme: "Bearer",
|
|
||||||
credentials: this.currentUser.authToken
|
|
||||||
},
|
|
||||||
type: "sendMessage"
|
|
||||||
} satisfies SendMessageRequest);
|
|
||||||
if (response.error) {
|
|
||||||
console.error("Error sending message:", response.error);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const form = new FormData();
|
await sendMessageWithFiles(content, replyToId ?? null, files, this.currentUser.authToken);
|
||||||
form.append("payload", JSON.stringify({
|
|
||||||
content: content.trim(),
|
|
||||||
reply_to_id: replyToId ?? null
|
|
||||||
} satisfies SendMessageRequest["data"]));
|
|
||||||
for (const f of files) form.append("files", f, f.name);
|
|
||||||
const res = await fetch(`${API_BASE_URL}/send_message`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: getAuthHeaders(this.currentUser.authToken, false),
|
|
||||||
body: form
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
console.error("Error sending message with files", await res.text());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending message:", error);
|
console.error("Error sending message:", error);
|
||||||
|
|||||||
Reference in New Issue
Block a user