Dev/v1.0 (#1)
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local> Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local> Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { SERVER_URL } from "./constants";
|
||||
|
||||
export type Tenant = {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
welcomeMessage: string;
|
||||
font: string | null;
|
||||
logo?: { id: number; base64Logo: string | null } | null;
|
||||
};
|
||||
|
||||
export type Branch = {
|
||||
id: number;
|
||||
name: string;
|
||||
tellerStations: { id: number; name: string }[];
|
||||
};
|
||||
|
||||
export type Service = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Ticket = {
|
||||
id: number;
|
||||
number: string;
|
||||
createdAt: string;
|
||||
service: Service;
|
||||
branch: Branch;
|
||||
station: { id: number; name: string } | null;
|
||||
};
|
||||
|
||||
export type TicketResponse = {
|
||||
ticket: Ticket;
|
||||
stations: { id: number; name: string }[];
|
||||
};
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${SERVER_URL}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed (${response.status}) for ${path}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export function getTenant(code: string) {
|
||||
return request<Tenant>(`/api/v1/tenants/${encodeURIComponent(code)}`);
|
||||
}
|
||||
|
||||
export function getBranches(tenantCode: string) {
|
||||
return request<Branch[]>(
|
||||
`/api/v1/branches/${encodeURIComponent(tenantCode)}`
|
||||
);
|
||||
}
|
||||
|
||||
export function getBranchServices(tenantCode: string, branchId: number) {
|
||||
return request<Service[]>(
|
||||
`/api/v1/branches/${encodeURIComponent(tenantCode)}/${branchId}/services`
|
||||
);
|
||||
}
|
||||
|
||||
export function createTicket(input: {
|
||||
branchId: number;
|
||||
serviceId: number;
|
||||
deviceToken: string;
|
||||
}) {
|
||||
return request<TicketResponse>("/api/v1/tickets", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function getTicketById(ticketId: number | string) {
|
||||
return request<Ticket>(`/api/v1/tickets/${ticketId}`);
|
||||
}
|
||||
|
||||
export function getTicketsForDevice(deviceToken: string) {
|
||||
return request<TicketResponse[]>(
|
||||
`/api/v1/tickets/devices/${encodeURIComponent(deviceToken)}`
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
const DEFAULT_QR_SECRET = "qms-branch-qr-v1";
|
||||
|
||||
function getQrSecret() {
|
||||
return process.env.QR_TOKEN_SECRET || DEFAULT_QR_SECRET;
|
||||
}
|
||||
|
||||
function toBase64Url(bytes: Uint8Array) {
|
||||
let binary = "";
|
||||
bytes.forEach((byte) => {
|
||||
binary += String.fromCharCode(byte);
|
||||
});
|
||||
return btoa(binary)
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/g, "");
|
||||
}
|
||||
|
||||
function fromBase64Url(token: string) {
|
||||
const padded = token.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padLength = (4 - (padded.length % 4)) % 4;
|
||||
const base64 = padded + "=".repeat(padLength);
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i += 1) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function xorBytes(bytes: Uint8Array, secret: string) {
|
||||
const key = new TextEncoder().encode(secret);
|
||||
return bytes.map((byte, index) => byte ^ key[index % key.length]);
|
||||
}
|
||||
|
||||
export type BranchQrPayload = {
|
||||
tenantCode: string;
|
||||
branchId: number;
|
||||
};
|
||||
|
||||
export function encodeBranchQrToken(
|
||||
tenantCode: string,
|
||||
branchId: number,
|
||||
secret = getQrSecret()
|
||||
) {
|
||||
const plain = `v1:${tenantCode.trim().toUpperCase()}:${Number(branchId)}`;
|
||||
const plainBytes = new TextEncoder().encode(plain);
|
||||
return toBase64Url(xorBytes(plainBytes, secret));
|
||||
}
|
||||
|
||||
export function decodeBranchQrToken(
|
||||
token: string,
|
||||
secret = getQrSecret()
|
||||
): BranchQrPayload | null {
|
||||
try {
|
||||
const plain = new TextDecoder().decode(
|
||||
xorBytes(fromBase64Url(token), secret)
|
||||
);
|
||||
const match = /^v1:([^:]+):(\d+)$/.exec(plain);
|
||||
if (!match) return null;
|
||||
return {
|
||||
tenantCode: match[1],
|
||||
branchId: Number(match[2]),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const SERVER_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
export const DEVICE_TOKEN_KEY = "qms_device_token";
|
||||
@@ -0,0 +1,20 @@
|
||||
import { DEVICE_TOKEN_KEY } from "./constants";
|
||||
|
||||
export function getDeviceToken(): string {
|
||||
if (typeof window === "undefined") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const existing = window.localStorage.getItem(DEVICE_TOKEN_KEY);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const token =
|
||||
typeof crypto !== "undefined" && "randomUUID" in crypto
|
||||
? crypto.randomUUID()
|
||||
: `web-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
window.localStorage.setItem(DEVICE_TOKEN_KEY, token);
|
||||
return token;
|
||||
}
|
||||
Reference in New Issue
Block a user