Merge branch 'master' of https://gitlab.forthrd.io/Pakin/supra-app into dev
This commit is contained in:
commit
b5e0705f79
14 changed files with 262 additions and 103 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -66,6 +66,7 @@
|
||||||
"@dnd-kit/abstract": "^0.2.4",
|
"@dnd-kit/abstract": "^0.2.4",
|
||||||
"@dnd-kit/helpers": "^0.2.4",
|
"@dnd-kit/helpers": "^0.2.4",
|
||||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||||
|
"@types/semver": "^7.7.1",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"@yume-chan/adb": "^2.6.0",
|
"@yume-chan/adb": "^2.6.0",
|
||||||
"@yume-chan/adb-credential-web": "^2.1.0",
|
"@yume-chan/adb-credential-web": "^2.1.0",
|
||||||
|
|
@ -77,6 +78,7 @@
|
||||||
"firebase": "^12.14.0",
|
"firebase": "^12.14.0",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"mode-watcher": "^1.1.0",
|
"mode-watcher": "^1.1.0",
|
||||||
|
"semver": "^7.8.4",
|
||||||
"usb": "^2.17.0",
|
"usb": "^2.17.0",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
|
|
|
||||||
|
|
@ -183,9 +183,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSheetPrice() {
|
async function saveSheetPrice() {
|
||||||
if (!canEditSheetPrice || sheetPriceValue === null) return;
|
if (!canEditSheetPrice || sheetPriceValue === null) return;
|
||||||
sendCommandRequest('sheet', {
|
await sendCommandRequest('sheet', {
|
||||||
country: get(departmentStore),
|
country: get(departmentStore),
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ enum ValueEvent {
|
||||||
SAVED
|
SAVED
|
||||||
}
|
}
|
||||||
|
|
||||||
function actionReport(action_name: string, values: any, currentRef: string) {
|
async function actionReport(action_name: string, values: any, currentRef: string) {
|
||||||
let country = get(departmentStore) ?? 'unknown dep';
|
let country = get(departmentStore) ?? 'unknown dep';
|
||||||
|
|
||||||
if (currentRef === 'brew') {
|
if (currentRef === 'brew') {
|
||||||
|
|
@ -27,7 +27,7 @@ function actionReport(action_name: string, values: any, currentRef: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage({
|
await sendMessage({
|
||||||
type: 'log_report',
|
type: 'log_report',
|
||||||
payload: {
|
payload: {
|
||||||
user: get(auth)?.email ?? 'unknown',
|
user: get(auth)?.email ?? 'unknown',
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@
|
||||||
let formatted = formatCustomDate(date);
|
let formatted = formatCustomDate(date);
|
||||||
ready_to_send_brew[0].LastChange = formatted;
|
ready_to_send_brew[0].LastChange = formatted;
|
||||||
|
|
||||||
sendMessage({
|
await sendMessage({
|
||||||
type: 'save_recipe',
|
type: 'save_recipe',
|
||||||
payload: {
|
payload: {
|
||||||
user_info,
|
user_info,
|
||||||
|
|
@ -194,7 +194,7 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (get(referenceFromPage) == 'overview') {
|
} else if (get(referenceFromPage) == 'overview') {
|
||||||
sendMessage({
|
await sendMessage({
|
||||||
type: 'save_recipe',
|
type: 'save_recipe',
|
||||||
payload: {
|
payload: {
|
||||||
user_info,
|
user_info,
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export async function getRecipes() {
|
||||||
recipeData.set([]);
|
recipeData.set([]);
|
||||||
recipeOverviewData.set([]);
|
recipeOverviewData.set([]);
|
||||||
|
|
||||||
sendMessage({
|
await sendMessage({
|
||||||
type: 'recipe',
|
type: 'recipe',
|
||||||
payload: {
|
payload: {
|
||||||
auth: idToken ?? '',
|
auth: idToken ?? '',
|
||||||
|
|
@ -82,7 +82,7 @@ export async function getRecipeWithVersion(version: string) {
|
||||||
|
|
||||||
// NOTE: although version is provided, actual version field is still need to be latest
|
// NOTE: although version is provided, actual version field is still need to be latest
|
||||||
// Just in case version is not found
|
// Just in case version is not found
|
||||||
sendMessage({
|
await sendMessage({
|
||||||
type: 'recipe',
|
type: 'recipe',
|
||||||
payload: {
|
payload: {
|
||||||
auth: idToken ?? '',
|
auth: idToken ?? '',
|
||||||
|
|
|
||||||
|
|
@ -39,15 +39,22 @@ import { buildOverviewFromServer } from '$lib/data/recipeService';
|
||||||
import { auth } from '../client/firebase';
|
import { auth } from '../client/firebase';
|
||||||
import { type RecipeVersion } from '$lib/models/recipe_version.model';
|
import { type RecipeVersion } from '$lib/models/recipe_version.model';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { socketAlreadySendHeartbeat, socketConnectionOfflineCount } from '../stores/websocketStore';
|
import {
|
||||||
|
sharedKey as sharedKey,
|
||||||
|
socketAlreadySendHeartbeat,
|
||||||
|
socketConnectionOfflineCount
|
||||||
|
} from '../stores/websocketStore';
|
||||||
import type { RecipePrice } from '$lib/models/price.model';
|
import type { RecipePrice } from '$lib/models/price.model';
|
||||||
import { sendCommandRequest, sendMessage } from './ws_messageSender';
|
import { sendCommandRequest, sendMessage } from './ws_messageSender';
|
||||||
import { auth as authStore } from '../stores/auth';
|
import { auth as authStore } from '../stores/auth';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { handleSheetResponseFromNoti } from './sheetNotiHandler';
|
import { handleSheetResponseFromNoti } from './sheetNotiHandler';
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
import { WebCryptoHelper } from '../utils/crypto';
|
||||||
|
|
||||||
export const messages = writable<string[]>([]);
|
export const messages = writable<string[]>([]);
|
||||||
|
|
||||||
|
type HandshakeAck = { server_public_key: string; status: string };
|
||||||
type WSMessage = { type: string; payload: any };
|
type WSMessage = { type: string; payload: any };
|
||||||
|
|
||||||
// MAXIMUM LIMIT = 1814355
|
// MAXIMUM LIMIT = 1814355
|
||||||
|
|
@ -133,7 +140,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
stream_data_end: (p) => {
|
stream_data_end: async (p) => {
|
||||||
recipeLoading.set(false);
|
recipeLoading.set(false);
|
||||||
|
|
||||||
// build overview for recipe from server
|
// build overview for recipe from server
|
||||||
|
|
@ -156,7 +163,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// send next chain message
|
// send next chain message
|
||||||
sendMessage({
|
await sendMessage({
|
||||||
type: 'price',
|
type: 'price',
|
||||||
payload: {
|
payload: {
|
||||||
action: {
|
action: {
|
||||||
|
|
@ -394,7 +401,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
currentRecipeVersionsSelector.set(result);
|
currentRecipeVersionsSelector.set(result);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
price: (p) => {
|
price: async (p) => {
|
||||||
let req_action = p.req_action;
|
let req_action = p.req_action;
|
||||||
let status = p.status;
|
let status = p.status;
|
||||||
let to = p.to;
|
let to = p.to;
|
||||||
|
|
@ -427,7 +434,7 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
current_streaming_instance[request_id] = '';
|
current_streaming_instance[request_id] = '';
|
||||||
streamingRawData.set(current_streaming_instance);
|
streamingRawData.set(current_streaming_instance);
|
||||||
|
|
||||||
sendCommandRequest('sheet', {
|
await sendCommandRequest('sheet', {
|
||||||
country: current_meta?.country ?? '',
|
country: current_meta?.country ?? '',
|
||||||
content: saved_product_code_to_get_from_sheet,
|
content: saved_product_code_to_get_from_sheet,
|
||||||
param: 'price',
|
param: 'price',
|
||||||
|
|
@ -527,24 +534,60 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function handleIncomingMessages(raw: string) {
|
function isSecuredAppVersion(version: string | undefined) {
|
||||||
const msg: WSMessage = JSON.parse(raw);
|
return version?.startsWith('0.0.2') ?? false;
|
||||||
if (msg.type !== 'heartbeat') {
|
}
|
||||||
console.log(`[WS MSG] type=${msg.type}`, msg.payload);
|
|
||||||
}
|
export async function handleIncomingMessages(raw: string, clientPrivateKey?: CryptoKey) {
|
||||||
if (msg == null) {
|
const APP_VERSION = env.PUBLIC_APP_SEMVER;
|
||||||
// error response
|
const parsedMessage = JSON.parse(raw);
|
||||||
addNotification('ERR:No response from server');
|
|
||||||
|
const ack: HandshakeAck = parsedMessage;
|
||||||
|
if (ack != null && ack.status === 'authenticated') {
|
||||||
|
// has server response
|
||||||
|
if (!clientPrivateKey) return;
|
||||||
|
|
||||||
|
sharedKey.set(await WebCryptoHelper.deriveSharedKey(clientPrivateKey, ack.server_public_key));
|
||||||
|
|
||||||
|
addNotification('INFO:Secured Connection');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isSecuredAppVersion(APP_VERSION) && parsedMessage.ciphertext && parsedMessage.iv) {
|
||||||
|
// secured message decryption
|
||||||
|
let sharedKeyStore = get(sharedKey);
|
||||||
|
if (sharedKeyStore) {
|
||||||
|
let decrypted_string = await WebCryptoHelper.decryptMessage(
|
||||||
|
sharedKeyStore,
|
||||||
|
parsedMessage.ciphertext,
|
||||||
|
parsedMessage.iv
|
||||||
|
);
|
||||||
|
let actual_message: WSMessage = JSON.parse(decrypted_string);
|
||||||
|
if (actual_message.type !== 'heartbeat') {
|
||||||
|
console.log(`[WS MSG] type=${actual_message.type}`, actual_message.payload);
|
||||||
|
}
|
||||||
|
|
||||||
// raw streaming type
|
handlers[actual_message.type]?.(actual_message.payload);
|
||||||
// if (msg.type.startsWith('raw_stream')) {
|
}
|
||||||
// // convert
|
} else {
|
||||||
// let sub_type = msg.type.replace('raw_stream_', '');
|
const msg: WSMessage = parsedMessage;
|
||||||
// msg.payload.sub_type = sub_type;
|
if (msg.type !== 'heartbeat') {
|
||||||
// msg.type = 'raw_stream';
|
console.log(`[WS MSG] type=${msg.type}`, msg.payload);
|
||||||
// }
|
}
|
||||||
|
if (msg == null) {
|
||||||
|
// error response
|
||||||
|
addNotification('ERR:No response from server');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
handlers[msg.type]?.(msg.payload);
|
// raw streaming type
|
||||||
|
// if (msg.type.startsWith('raw_stream')) {
|
||||||
|
// // convert
|
||||||
|
// let sub_type = msg.type.replace('raw_stream_', '');
|
||||||
|
// msg.payload.sub_type = sub_type;
|
||||||
|
// msg.type = 'raw_stream';
|
||||||
|
// }
|
||||||
|
|
||||||
|
handlers[msg.type]?.(msg.payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import type { OutMessage } from '../types/outMessage';
|
import type { OutMessage } from '../types/outMessage';
|
||||||
import { socketStore } from '../stores/websocketStore';
|
import { sharedKey, socketStore } from '../stores/websocketStore';
|
||||||
import { addNotification } from '../stores/noti';
|
import { addNotification } from '../stores/noti';
|
||||||
import { auth } from '../stores/auth';
|
import { auth } from '../stores/auth';
|
||||||
|
import { WebCryptoHelper } from '../utils/crypto';
|
||||||
|
import * as semver from 'semver';
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
|
||||||
export const queue = writable<string[]>([]);
|
export const queue = writable<string[]>([]);
|
||||||
|
|
||||||
|
|
@ -18,7 +21,7 @@ function getServiceName(cmdReq: CommandRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Websocket message wrapper for commands like `sheet`, `command`
|
// Websocket message wrapper for commands like `sheet`, `command`
|
||||||
export function sendCommandRequest(target: CommandRequest, values: any): boolean {
|
export async function sendCommandRequest(target: CommandRequest, values: any): Promise<boolean> {
|
||||||
let srv_name = getServiceName(target);
|
let srv_name = getServiceName(target);
|
||||||
let curr_user = get(auth);
|
let curr_user = get(auth);
|
||||||
|
|
||||||
|
|
@ -31,7 +34,7 @@ export function sendCommandRequest(target: CommandRequest, values: any): boolean
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendMessage({
|
return await sendMessage({
|
||||||
type: target,
|
type: target,
|
||||||
payload: {
|
payload: {
|
||||||
user_info: user_info ?? {},
|
user_info: user_info ?? {},
|
||||||
|
|
@ -41,9 +44,13 @@ export function sendCommandRequest(target: CommandRequest, values: any): boolean
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendMessage(msg: OutMessage, ignore_queue_request: boolean = true): boolean {
|
export async function sendMessage(
|
||||||
|
msg: OutMessage,
|
||||||
|
ignore_queue_request: boolean = true
|
||||||
|
): Promise<boolean> {
|
||||||
|
const APP_VERSION = env.PUBLIC_APP_SEMVER;
|
||||||
const socket = get(socketStore);
|
const socket = get(socketStore);
|
||||||
const data = JSON.stringify(msg);
|
let data = JSON.stringify(msg);
|
||||||
|
|
||||||
// console.log('try sending ', data);
|
// console.log('try sending ', data);
|
||||||
|
|
||||||
|
|
@ -64,6 +71,17 @@ export function sendMessage(msg: OutMessage, ignore_queue_request: boolean = tru
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log('send v2', APP_VERSION, semver.satisfies(APP_VERSION, '^0.0.2'));
|
||||||
|
|
||||||
|
if (semver.satisfies(APP_VERSION, '^0.0.2')) {
|
||||||
|
console.log('sending secured');
|
||||||
|
let sharedKeyRes = get(sharedKey);
|
||||||
|
|
||||||
|
// do encrypt
|
||||||
|
if (sharedKeyRes != null)
|
||||||
|
data = JSON.stringify(await WebCryptoHelper.encryptMessage(sharedKeyRes, data));
|
||||||
|
}
|
||||||
|
|
||||||
socket.send(data);
|
socket.send(data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@ import {
|
||||||
import type { PriceSlot } from '../stores/sheetStore';
|
import type { PriceSlot } from '../stores/sheetStore';
|
||||||
import { setGenLayoutGenerating } from '../stores/genLayoutStore';
|
import { setGenLayoutGenerating } from '../stores/genLayoutStore';
|
||||||
|
|
||||||
export function requestCatalogs(country: string): boolean {
|
export async function requestCatalogs(country: string): Promise<boolean> {
|
||||||
return sendCommandRequest('sheet', {
|
return await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
param: 'catalogs'
|
param: 'catalogs'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestPriceSlots(country: string): boolean {
|
export async function requestPriceSlots(country: string): Promise<boolean> {
|
||||||
setPendingPriceSlotsCountry(country);
|
setPendingPriceSlotsCountry(country);
|
||||||
resetPriceSlotsCountry(country);
|
resetPriceSlotsCountry(country);
|
||||||
const request_id = crypto.randomUUID();
|
const request_id = crypto.randomUUID();
|
||||||
|
|
@ -46,7 +46,7 @@ export function requestPriceSlots(country: string): boolean {
|
||||||
request_id
|
request_id
|
||||||
};
|
};
|
||||||
console.log('[sheetService] Sending PriceSlot request:', values);
|
console.log('[sheetService] Sending PriceSlot request:', values);
|
||||||
const sent = sendCommandRequest('sheet', values);
|
const sent = await sendCommandRequest('sheet', values);
|
||||||
console.log('[sheetService] PriceSlot request sent:', sent);
|
console.log('[sheetService] PriceSlot request sent:', sent);
|
||||||
if (!sent) {
|
if (!sent) {
|
||||||
priceSlotsLoading.set(false);
|
priceSlotsLoading.set(false);
|
||||||
|
|
@ -54,48 +54,52 @@ export function requestPriceSlots(country: string): boolean {
|
||||||
return sent;
|
return sent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updatePriceSlot(country: string, content: PriceSlot): boolean {
|
export async function updatePriceSlot(country: string, content: PriceSlot): Promise<boolean> {
|
||||||
return sendCommandRequest('sheet', {
|
return await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
content: content,
|
content: content,
|
||||||
param: 'update/priceslot'
|
param: 'update/priceslot'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enterRoom(country: string, catalog: string): boolean {
|
export async function enterRoom(country: string, catalog: string): Promise<boolean> {
|
||||||
return sendCommandRequest('sheet', {
|
return await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
param: 'enter'
|
param: 'enter'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendHeartbeat(country: string, catalog: string): boolean {
|
export async function sendHeartbeat(country: string, catalog: string): Promise<boolean> {
|
||||||
return sendCommandRequest('sheet', {
|
return await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
param: 'heartbeat'
|
param: 'heartbeat'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exitRoom(country: string, catalog: string): boolean {
|
export async function exitRoom(country: string, catalog: string): Promise<boolean> {
|
||||||
return sendCommandRequest('sheet', {
|
return await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
param: 'exit'
|
param: 'exit'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestCatalogMenu(country: string, catalog: string): boolean {
|
export async function requestCatalogMenu(country: string, catalog: string): Promise<boolean> {
|
||||||
return sendCommandRequest('sheet', {
|
return await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
param: 'catalog/menu'
|
param: 'catalog/menu'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateMenu(country: string, catalog: string, content: any[]): boolean {
|
export async function updateMenu(
|
||||||
return sendCommandRequest('sheet', {
|
country: string,
|
||||||
|
catalog: string,
|
||||||
|
content: any[]
|
||||||
|
): Promise<boolean> {
|
||||||
|
return await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
content: content,
|
content: content,
|
||||||
|
|
@ -103,9 +107,9 @@ export function updateMenu(country: string, catalog: string, content: any[]): bo
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addMenu(country: string, catalog: string, content: any[]): boolean {
|
export async function addMenu(country: string, catalog: string, content: any[]): Promise<boolean> {
|
||||||
console.log('[sheetService] Adding menu:', { country, catalog, content });
|
console.log('[sheetService] Adding menu:', { country, catalog, content });
|
||||||
const sent = sendCommandRequest('sheet', {
|
const sent = await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
content: content,
|
content: content,
|
||||||
|
|
@ -115,9 +119,13 @@ export function addMenu(country: string, catalog: string, content: any[]): boole
|
||||||
return sent;
|
return sent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteMenu(country: string, catalog: string, targetIds: number[]): boolean {
|
export async function deleteMenu(
|
||||||
|
country: string,
|
||||||
|
catalog: string,
|
||||||
|
targetIds: number[]
|
||||||
|
): Promise<boolean> {
|
||||||
const content = targetIds.map((id) => ({ target_id: id }));
|
const content = targetIds.map((id) => ({ target_id: id }));
|
||||||
return sendCommandRequest('sheet', {
|
return await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
content: content,
|
content: content,
|
||||||
|
|
@ -125,12 +133,12 @@ export function deleteMenu(country: string, catalog: string, targetIds: number[]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function swapMenu(
|
export async function swapMenu(
|
||||||
country: string,
|
country: string,
|
||||||
catalog: string,
|
catalog: string,
|
||||||
swaps: { source_id: number; target_id: number }[]
|
swaps: { source_id: number; target_id: number }[]
|
||||||
): boolean {
|
): Promise<boolean> {
|
||||||
return sendCommandRequest('sheet', {
|
return await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
content: swaps,
|
content: swaps,
|
||||||
|
|
@ -138,7 +146,7 @@ export function swapMenu(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestListMenu(country: string, boxid?: string): boolean {
|
export async function requestListMenu(country: string, boxid?: string): Promise<boolean> {
|
||||||
const curr_user = get(auth);
|
const curr_user = get(auth);
|
||||||
|
|
||||||
let user_info: any = {};
|
let user_info: any = {};
|
||||||
|
|
@ -155,7 +163,7 @@ export function requestListMenu(country: string, boxid?: string): boolean {
|
||||||
|
|
||||||
console.log('[sheetService] Sending list_menu request for country:', country, 'boxid:', boxid);
|
console.log('[sheetService] Sending list_menu request for country:', country, 'boxid:', boxid);
|
||||||
|
|
||||||
return sendMessage({
|
return await sendMessage({
|
||||||
type: 'list_menu',
|
type: 'list_menu',
|
||||||
payload: {
|
payload: {
|
||||||
user_info,
|
user_info,
|
||||||
|
|
@ -165,7 +173,7 @@ export function requestListMenu(country: string, boxid?: string): boolean {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestGenLayout(country: string): boolean {
|
export async function requestGenLayout(country: string): Promise<boolean> {
|
||||||
const curr_user = get(auth);
|
const curr_user = get(auth);
|
||||||
|
|
||||||
let user_info: any = {};
|
let user_info: any = {};
|
||||||
|
|
@ -181,7 +189,7 @@ export function requestGenLayout(country: string): boolean {
|
||||||
|
|
||||||
console.log('[sheetService] Sending gen-layout request for country:', country);
|
console.log('[sheetService] Sending gen-layout request for country:', country);
|
||||||
|
|
||||||
return sendMessage({
|
return await sendMessage({
|
||||||
type: 'command',
|
type: 'command',
|
||||||
payload: {
|
payload: {
|
||||||
user_info,
|
user_info,
|
||||||
|
|
@ -200,7 +208,7 @@ export function requestGenLayout(country: string): boolean {
|
||||||
* Request price data from sheet for specific product codes
|
* Request price data from sheet for specific product codes
|
||||||
* NOTE: Can only send once per type (price). Use hasSheetPriceBeenSent to check.
|
* NOTE: Can only send once per type (price). Use hasSheetPriceBeenSent to check.
|
||||||
*/
|
*/
|
||||||
export function requestSheetPrice(country: string, productCodes: string[]): boolean {
|
export async function requestSheetPrice(country: string, productCodes: string[]): Promise<boolean> {
|
||||||
// Check if already sent
|
// Check if already sent
|
||||||
if (hasSheetPriceBeenSent('price')) {
|
if (hasSheetPriceBeenSent('price')) {
|
||||||
console.warn('[sheetService] Price request already sent, skipping');
|
console.warn('[sheetService] Price request already sent, skipping');
|
||||||
|
|
@ -240,7 +248,7 @@ export function requestSheetPrice(country: string, productCodes: string[]): bool
|
||||||
request_id
|
request_id
|
||||||
);
|
);
|
||||||
|
|
||||||
const sent = sendCommandRequest('sheet', {
|
const sent = await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
content: content,
|
content: content,
|
||||||
param: 'price',
|
param: 'price',
|
||||||
|
|
@ -261,10 +269,10 @@ export function requestSheetPrice(country: string, productCodes: string[]): bool
|
||||||
* Update price data in sheet
|
* Update price data in sheet
|
||||||
* content: [{ row_index: number, cells: [{ value: string, coord: { row: number, col: number } }] }]
|
* content: [{ row_index: number, cells: [{ value: string, coord: { row: number, col: number } }] }]
|
||||||
*/
|
*/
|
||||||
export function updateSheetPrice(
|
export async function updateSheetPrice(
|
||||||
country: string,
|
country: string,
|
||||||
content: { row_index: number; cells: { value: string; coord: { row: number; col: number } }[] }[]
|
content: { row_index: number; cells: { value: string; coord: { row: number; col: number } }[] }[]
|
||||||
): boolean {
|
): Promise<boolean> {
|
||||||
if (!content || content.length === 0) {
|
if (!content || content.length === 0) {
|
||||||
console.warn('[sheetService] No content to update');
|
console.warn('[sheetService] No content to update');
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -277,7 +285,7 @@ export function updateSheetPrice(
|
||||||
content.length
|
content.length
|
||||||
);
|
);
|
||||||
|
|
||||||
return sendCommandRequest('sheet', {
|
return await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
content: content,
|
content: content,
|
||||||
param: 'update/price'
|
param: 'update/price'
|
||||||
|
|
@ -288,7 +296,10 @@ export function updateSheetPrice(
|
||||||
* Add new price rows to sheet (for product codes that don't exist in price sheet)
|
* Add new price rows to sheet (for product codes that don't exist in price sheet)
|
||||||
* content: [{ cells: [product_code, name_en, name_th, ..., price, ...] }]
|
* content: [{ cells: [product_code, name_en, name_th, ..., price, ...] }]
|
||||||
*/
|
*/
|
||||||
export function addSheetPrice(country: string, content: { cells: string[] }[]): boolean {
|
export async function addSheetPrice(
|
||||||
|
country: string,
|
||||||
|
content: { cells: string[] }[]
|
||||||
|
): Promise<boolean> {
|
||||||
if (!content || content.length === 0) {
|
if (!content || content.length === 0) {
|
||||||
console.warn('[sheetService] No content to add');
|
console.warn('[sheetService] No content to add');
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -302,7 +313,7 @@ export function addSheetPrice(country: string, content: { cells: string[] }[]):
|
||||||
content
|
content
|
||||||
);
|
);
|
||||||
|
|
||||||
return sendCommandRequest('sheet', {
|
return await sendCommandRequest('sheet', {
|
||||||
country: country,
|
country: country,
|
||||||
content: content,
|
content: content,
|
||||||
param: 'add/price'
|
param: 'add/price'
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,20 @@ import { auth } from '../client/firebase';
|
||||||
import { auth as authStore } from '$lib/core/stores/auth';
|
import { auth as authStore } from '$lib/core/stores/auth';
|
||||||
import { addNotification } from './noti';
|
import { addNotification } from './noti';
|
||||||
import { permission } from './permissions';
|
import { permission } from './permissions';
|
||||||
|
import { WebCryptoHelper } from '../utils/crypto';
|
||||||
|
|
||||||
let socket: WebSocket | null = null;
|
let socket: WebSocket | null = null;
|
||||||
let reconnectTimeout: any;
|
let reconnectTimeout: any;
|
||||||
let socketCheck: any;
|
let socketCheck: any;
|
||||||
|
let sendAuthInfoInterval: any;
|
||||||
const ENABLE_WS_DEBUG: boolean = false;
|
const ENABLE_WS_DEBUG: boolean = false;
|
||||||
|
|
||||||
export const socketConnectionOfflineCount = writable<number>(0);
|
export const socketConnectionOfflineCount = writable<number>(0);
|
||||||
export const socketAlreadySendHeartbeat = writable<number>(0);
|
export const socketAlreadySendHeartbeat = writable<number>(0);
|
||||||
export const socketStore = writable<WebSocket | null>(null);
|
export const socketStore = writable<WebSocket | null>(null);
|
||||||
|
|
||||||
|
export const sharedKey = writable<CryptoKey | null>(null);
|
||||||
|
|
||||||
export function waitForOpenSocket(timeoutMs = 8000): Promise<WebSocket | null> {
|
export function waitForOpenSocket(timeoutMs = 8000): Promise<WebSocket | null> {
|
||||||
const currentSocket = get(socketStore);
|
const currentSocket = get(socketStore);
|
||||||
if (currentSocket?.readyState === WebSocket.OPEN) {
|
if (currentSocket?.readyState === WebSocket.OPEN) {
|
||||||
|
|
@ -49,7 +53,7 @@ export function waitForOpenSocket(timeoutMs = 8000): Promise<WebSocket | null> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function connectToWebsocket(id_token?: string) {
|
export async function connectToWebsocket(id_token?: string) {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
// console.log('connecting to ', env.PUBLIC_WSS);
|
// console.log('connecting to ', env.PUBLIC_WSS);
|
||||||
try {
|
try {
|
||||||
|
|
@ -57,12 +61,12 @@ export function connectToWebsocket(id_token?: string) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let productionMode = env.PUBLIC_WSS.startsWith('wss');
|
let ws_url = env.PUBLIC_WSS;
|
||||||
|
|
||||||
let ws_url = productionMode ? `${env.PUBLIC_WSS}?token=${id_token}` : `${env.PUBLIC_WSS}`;
|
|
||||||
socket = new WebSocket(ws_url);
|
socket = new WebSocket(ws_url);
|
||||||
|
sharedKey.set(null);
|
||||||
|
const { privateKey, publicKeyBase64 } = await WebCryptoHelper.generateKeyPair();
|
||||||
|
|
||||||
socket.addEventListener('open', () => {
|
socket.addEventListener('open', async () => {
|
||||||
socketStore.set(socket);
|
socketStore.set(socket);
|
||||||
addNotification('INFO:Connected!');
|
addNotification('INFO:Connected!');
|
||||||
|
|
||||||
|
|
@ -74,29 +78,40 @@ export function connectToWebsocket(id_token?: string) {
|
||||||
let auth_data = get(authStore);
|
let auth_data = get(authStore);
|
||||||
let perms = get(permission);
|
let perms = get(permission);
|
||||||
|
|
||||||
// Debug: check if auth_data has uid
|
socket.send(
|
||||||
console.log('[WS Auth] Sending auth with:', {
|
JSON.stringify({
|
||||||
uid: auth_data?.uid,
|
token: id_token ?? '',
|
||||||
name: auth_data?.displayName,
|
client_public_key: publicKeyBase64
|
||||||
email: auth_data?.email
|
})
|
||||||
});
|
);
|
||||||
|
|
||||||
sendMessage({
|
sendAuthInfoInterval = setInterval(async () => {
|
||||||
type: 'auth',
|
if (get(sharedKey)) {
|
||||||
payload: {
|
// Debug: check if auth_data has uid
|
||||||
user: {
|
console.log('[WS Auth] Sending auth info with:', {
|
||||||
uid: auth_data?.uid ?? '',
|
uid: auth_data?.uid,
|
||||||
name: auth_data?.displayName ?? '',
|
name: auth_data?.displayName,
|
||||||
email: auth_data?.email ?? '',
|
email: auth_data?.email
|
||||||
permissions: perms.join(',')
|
});
|
||||||
}
|
await sendMessage({
|
||||||
|
type: 'auth',
|
||||||
|
payload: {
|
||||||
|
user: {
|
||||||
|
uid: auth_data?.uid ?? '',
|
||||||
|
name: auth_data?.displayName ?? '',
|
||||||
|
email: auth_data?.email ?? '',
|
||||||
|
permissions: perms.join(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
clearInterval(sendAuthInfoInterval);
|
||||||
}
|
}
|
||||||
});
|
}, 3000);
|
||||||
}
|
}
|
||||||
console.log(socket);
|
console.log(socket);
|
||||||
|
|
||||||
// heartbeat 10s
|
// heartbeat 10s
|
||||||
socketCheck = setInterval(() => {
|
socketCheck = setInterval(async () => {
|
||||||
if (get(socketAlreadySendHeartbeat) > 0) {
|
if (get(socketAlreadySendHeartbeat) > 0) {
|
||||||
let heartbeat_may_offline_count = get(socketConnectionOfflineCount);
|
let heartbeat_may_offline_count = get(socketConnectionOfflineCount);
|
||||||
|
|
||||||
|
|
@ -108,13 +123,13 @@ export function connectToWebsocket(id_token?: string) {
|
||||||
socketConnectionOfflineCount.set(0);
|
socketConnectionOfflineCount.set(0);
|
||||||
socketAlreadySendHeartbeat.set(0);
|
socketAlreadySendHeartbeat.set(0);
|
||||||
|
|
||||||
connectToWebsocket(id_token);
|
await connectToWebsocket(id_token);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (socket != null) {
|
if (socket != null) {
|
||||||
sendMessage({
|
await sendMessage({
|
||||||
type: 'heartbeat',
|
type: 'heartbeat',
|
||||||
payload: {}
|
payload: {}
|
||||||
});
|
});
|
||||||
|
|
@ -130,18 +145,19 @@ export function connectToWebsocket(id_token?: string) {
|
||||||
if (auth.currentUser && socket == null) {
|
if (auth.currentUser && socket == null) {
|
||||||
console.log('try reconnect websocket ...');
|
console.log('try reconnect websocket ...');
|
||||||
// retry again
|
// retry again
|
||||||
reconnectTimeout = setTimeout(() => {
|
reconnectTimeout = setTimeout(async () => {
|
||||||
connectToWebsocket(id_token);
|
await connectToWebsocket(id_token);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener('message', (event) => {
|
socket.addEventListener('message', async (event) => {
|
||||||
handleIncomingMessages(event.data);
|
await handleIncomingMessages(event.data, privateKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener('close', () => {
|
socket.addEventListener('close', () => {
|
||||||
socketStore.set(null);
|
socketStore.set(null);
|
||||||
|
sharedKey.set(null);
|
||||||
socket = null;
|
socket = null;
|
||||||
|
|
||||||
clearInterval(socketCheck);
|
clearInterval(socketCheck);
|
||||||
|
|
@ -149,13 +165,14 @@ export function connectToWebsocket(id_token?: string) {
|
||||||
if (auth.currentUser && !socket) {
|
if (auth.currentUser && !socket) {
|
||||||
console.log('try reconnect websocket ...');
|
console.log('try reconnect websocket ...');
|
||||||
// retry again
|
// retry again
|
||||||
reconnectTimeout = setTimeout(() => connectToWebsocket(id_token), 5000);
|
reconnectTimeout = setTimeout(async () => await connectToWebsocket(id_token), 5000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener('error', (e) => {
|
socket.addEventListener('error', (e) => {
|
||||||
// console.log('WebSocket error: ', e);
|
// console.log('WebSocket error: ', e);
|
||||||
socketStore.set(null);
|
socketStore.set(null);
|
||||||
|
sharedKey.set(null);
|
||||||
});
|
});
|
||||||
} catch (socket_error: any) {
|
} catch (socket_error: any) {
|
||||||
if (ENABLE_WS_DEBUG) {
|
if (ENABLE_WS_DEBUG) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export type OutMessage =
|
export type OutMessage =
|
||||||
|
| { token: any; client_public_key: any }
|
||||||
| { type: 'chat'; payload: string }
|
| { type: 'chat'; payload: string }
|
||||||
| { type: 'ping' }
|
| { type: 'ping' }
|
||||||
| { type: 'lock'; payload: { field: string } }
|
| { type: 'lock'; payload: { field: string } }
|
||||||
|
|
@ -54,7 +55,7 @@ export type OutMessage =
|
||||||
values: any;
|
values: any;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'list_menu';
|
type: 'list_menu';
|
||||||
payload: {
|
payload: {
|
||||||
user_info: any;
|
user_info: any;
|
||||||
|
|
@ -62,7 +63,6 @@ export type OutMessage =
|
||||||
boxid?: string;
|
boxid?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
| {
|
| {
|
||||||
type: 'price';
|
type: 'price';
|
||||||
payload: {
|
payload: {
|
||||||
|
|
|
||||||
68
src/lib/core/utils/crypto.ts
Normal file
68
src/lib/core/utils/crypto.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
export class WebCryptoHelper {
|
||||||
|
static async generateKeyPair() {
|
||||||
|
const keyPair = await window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: 'ECDH',
|
||||||
|
namedCurve: 'P-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['deriveKey', 'deriveBits']
|
||||||
|
);
|
||||||
|
|
||||||
|
const exportedPublic = await window.crypto.subtle.exportKey('raw', keyPair.publicKey);
|
||||||
|
const publicKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(exportedPublic)));
|
||||||
|
|
||||||
|
return { privateKey: keyPair.privateKey, publicKeyBase64 };
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deriveSharedKey(clientPrivateKey: any, serverPublicKeyBase64: any) {
|
||||||
|
const binarySign = atob(serverPublicKeyBase64);
|
||||||
|
const bytes = new Uint8Array(binarySign.length);
|
||||||
|
for (let i = 0; i < binarySign.length; i++) {
|
||||||
|
bytes[i] = binarySign.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const importedServerPublic = await window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
bytes,
|
||||||
|
{ name: 'ECDH', namedCurve: 'P-256' },
|
||||||
|
true,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
return await window.crypto.subtle.deriveKey(
|
||||||
|
{ name: 'ECDH', public: importedServerPublic },
|
||||||
|
clientPrivateKey,
|
||||||
|
{ name: 'AES-GCM', length: 256 },
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async decryptMessage(aesKey: any, ciphertextBase64: any, ivBase64: any) {
|
||||||
|
const rawCipher = Uint8Array.from(atob(ciphertextBase64), (c) => c.charCodeAt(0));
|
||||||
|
const rawIv = Uint8Array.from(atob(ivBase64), (c) => c.charCodeAt(0));
|
||||||
|
const decryptedBuffer = await window.crypto.subtle.decrypt(
|
||||||
|
{ name: 'AES-GCM', iv: rawIv },
|
||||||
|
aesKey,
|
||||||
|
rawCipher
|
||||||
|
);
|
||||||
|
return new TextDecoder().decode(decryptedBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt outgoing messages before sending them to your Axum backend
|
||||||
|
static async encryptMessage(aesKey: any, plainText: any) {
|
||||||
|
const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 12-byte nonce
|
||||||
|
const encodedText = new TextEncoder().encode(plainText);
|
||||||
|
|
||||||
|
const ciphertextBuffer = await window.crypto.subtle.encrypt(
|
||||||
|
{ name: 'AES-GCM', iv: iv },
|
||||||
|
aesKey,
|
||||||
|
encodedText
|
||||||
|
);
|
||||||
|
|
||||||
|
const ciphertextBase64 = btoa(String.fromCharCode(...new Uint8Array(ciphertextBuffer)));
|
||||||
|
const ivBase64 = btoa(String.fromCharCode(...iv));
|
||||||
|
|
||||||
|
return { ciphertext: ciphertextBase64, iv: ivBase64 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -97,8 +97,8 @@
|
||||||
websocketConnectedForUid = currentUser.uid;
|
websocketConnectedForUid = currentUser.uid;
|
||||||
console.log('connect ws after auth ready');
|
console.log('connect ws after auth ready');
|
||||||
|
|
||||||
void currentUser.getIdToken().then((idToken) => {
|
void currentUser.getIdToken(true).then(async (idToken) => {
|
||||||
connectToWebsocket(idToken);
|
await connectToWebsocket(idToken);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,9 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function sendGetRecipeVersions(country: string) {
|
async function sendGetRecipeVersions(country: string) {
|
||||||
version_list = [];
|
version_list = [];
|
||||||
sendMessage({
|
await sendMessage({
|
||||||
type: 'recipe_versions',
|
type: 'recipe_versions',
|
||||||
payload: {
|
payload: {
|
||||||
auth: '',
|
auth: '',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue