diff --git a/modules/gui/package.json b/modules/gui/package.json index 10e68c7..9b5afd7 100644 --- a/modules/gui/package.json +++ b/modules/gui/package.json @@ -43,12 +43,14 @@ "dependencies": { "@tauri-apps/api": "^1.2.0", "@types/bcryptjs": "^2.4.2", + "@types/url-join": "^4.0.1", "bcryptjs": "^2.4.3", "buffer": "^6.0.3", "fuse.js": "^6.6.2", "lodash": "^4.17.21", "lorem-ipsum": "^2.0.8", - "svelte-watch-resize": "^1.0.3" + "svelte-watch-resize": "^1.0.3", + "url-join": "^5.0.0" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/modules/gui/src/components/TopBar/ProfileNavButton.svelte b/modules/gui/src/components/TopBar/ProfileNavButton.svelte index 1cb3e5a..7a79823 100644 --- a/modules/gui/src/components/TopBar/ProfileNavButton.svelte +++ b/modules/gui/src/components/TopBar/ProfileNavButton.svelte @@ -2,13 +2,13 @@ import { open } from '@tauri-apps/api/shell'; import { authStore } from '$libs/stores'; import type { Developer } from '@tea/ui/types'; - import { apiBaseUrl } from '@api'; + import { baseUrl } from '$libs/v1Client'; let user: Developer | null = null; const deviceId = authStore.deviceIdStore; const openGithub = () => { - open(`${apiBaseUrl}/auth/user?device_id=${$deviceId}`); + open(`${baseUrl}/auth/user?device_id=${$deviceId}`); try { authStore.pollSession(); } catch (error) { diff --git a/modules/gui/src/libs/api/mock.ts b/modules/gui/src/libs/api/mock.ts index 45beba2..bca61a8 100644 --- a/modules/gui/src/libs/api/mock.ts +++ b/modules/gui/src/libs/api/mock.ts @@ -11,8 +11,6 @@ import { PackageStates } from '../types'; import { loremIpsum } from 'lorem-ipsum'; import _ from 'lodash'; -export const apiBaseUrl = 'https://api.tea.xyz/v1'; - const packages: Package[] = [ { slug: 'mesonbuild_com', diff --git a/modules/gui/src/libs/api/tauri.ts b/modules/gui/src/libs/api/tauri.ts index 1800b95..62bfb7a 100644 --- a/modules/gui/src/libs/api/tauri.ts +++ b/modules/gui/src/libs/api/tauri.ts @@ -11,73 +11,22 @@ * - connect to a local platform api and returns a data */ import { getClient } from '@tauri-apps/api/http'; -// import { invoke } from '@tauri-apps/api'; -import { Command } from '@tauri-apps/api/shell'; -import { readDir, BaseDirectory } from '@tauri-apps/api/fs'; + +import urlJoin from 'url-join'; import type { Package, Review, AirtablePost, Developer, Bottle } from '@tea/ui/types'; -import type { GUIPackage, Course, Category, AuthStatus } from '../types'; +import type { GUIPackage, Course, Category, AuthStatus, DeviceAuth } from '../types'; import * as mock from './mock'; import { PackageStates } from '../types'; -import { getSession } from '$libs/stores/auth'; -import type { Session } from '$libs/stores/auth'; import { getInstalledPackages } from '$libs/teaDir'; -import bcrypt from 'bcryptjs'; +import { installPackageCommand } from '$libs/cli'; -export const apiBaseUrl = 'https://api.tea.xyz/v1'; -// export const apiBaseUrl = 'http://localhost:3000/v1'; - -async function getHeaders(path: string, session: Session) { - const unixMs = new Date().getTime(); - const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex - const deviceId = session.device_id?.split('-')[0]; - const preHash = [unixHexSecs, session.key, deviceId, path].join(''); - - const Authorization = bcrypt.hashSync(preHash, 10); - - return { - Authorization, - ['tea-ts']: unixMs.toString(), - ['tea-uid']: session.user?.developer_id, - ['tea-gui_id']: session.device_id - }; -} - -async function get(path: string, query?: { [key: string]: string }) { - const [session, client] = await Promise.all([getSession(), getClient()]); - - const uri = join(apiBaseUrl, path); - - const headers = - session?.device_id && session?.user - ? await getHeaders(`GET/${path}`, session) - : { Authorization: 'public ' }; - - const { data } = await client.get(uri.toString(), { - headers, - query: query || {} - }); - return data; -} - -const join = function (...paths: string[]) { - return paths - .map(function (path) { - if (path[0] === '/') { - path = path.slice(1); - } - if (path[path.length - 1] === '/') { - path = path.slice(0, path.length - 1); - } - return path; - }) - .join('/'); -}; +import { get as apiGet } from '$libs/v1Client'; export async function getPackages(): Promise { const [packages, installedPackages] = await Promise.all([ - get('packages'), + apiGet('packages'), getInstalledPackages() ]); @@ -98,7 +47,7 @@ export async function getFeaturedPackages(): Promise { export async function getPackageReviews(full_name: string): Promise { console.log(`getting reviews for ${full_name}`); - const reviews: Review[] = await get( + const reviews: Review[] = await apiGet( `packages/${full_name.replaceAll('/', ':')}/reviews` ); @@ -113,40 +62,8 @@ export async function installPackage(full_name: string) { } } -async function installPackageCommand(full_name: string) { - return new Promise((resolve, reject) => { - const teaInstallCommand = new Command('tea-install', [`+${full_name}`, 'true']); - teaInstallCommand.on('error', reject); - - const handleLineOutput = async (line: string | { code: number }) => { - const c = await child; - if ( - (typeof line === 'string' && line.includes('installed:')) || - (typeof line !== 'string' && line?.code === 0) - ) { - c.kill(); - resolve(c.pid); - } else if (typeof line !== 'string' && line?.code === 1) { - reject(); - } - }; - - teaInstallCommand.stdout.on('data', handleLineOutput); - teaInstallCommand.stderr.on('data', handleLineOutput); - teaInstallCommand.on('close', (line: string) => { - console.log('command closed!'); - handleLineOutput(line || ''); - }); - teaInstallCommand.on('error', (line: string) => { - console.log('command error!', line); - handleLineOutput(line || ''); - }); - const child = teaInstallCommand.spawn(); - }); -} - export async function getFeaturedCourses(): Promise { - const posts = await get('posts', { tag: 'featured_course' }); + const posts = await apiGet('posts', { tag: 'featured_course' }); return posts.map((post) => { return { title: post.title, @@ -164,36 +81,30 @@ export async function getTopPackages(): Promise { export async function getAllPosts(tag?: string): Promise { // add filter here someday: tag = news | course - const posts = await get('posts', tag ? { tag } : {}); + const posts = await apiGet('posts', tag ? { tag } : {}); return posts; } export async function getCategorizedPackages(): Promise { - const categories = await get('/packages/categorized'); + const categories = await apiGet('/packages/categorized'); return categories; } -type DeviceAuth = { - status: AuthStatus; - user: Developer; - key: string; -}; - export async function getDeviceAuth(deviceId: string): Promise { - const data = await get(`/auth/device/${deviceId}`); + const data = await apiGet(`/auth/device/${deviceId}`); return data; } export async function getPackageBottles(packageName: string): Promise { console.log('getting bottles for ', packageName); const client = await getClient(); - const uri = join('https://app.tea.xyz/api/bottles/', packageName); + const uri = urlJoin('https://app.tea.xyz/api/bottles/', packageName); const { data } = await client.get(uri.toString()); console.log('got bottles', data); return data; } export async function registerDevice(): Promise { - const { deviceId } = await get<{ deviceId: string }>('/auth/registerDevice'); + const { deviceId } = await apiGet<{ deviceId: string }>('/auth/registerDevice'); return deviceId; } diff --git a/modules/gui/src/libs/cli.ts b/modules/gui/src/libs/cli.ts new file mode 100644 index 0000000..262e3ed --- /dev/null +++ b/modules/gui/src/libs/cli.ts @@ -0,0 +1,33 @@ +import { Command } from '@tauri-apps/api/shell'; + +export async function installPackageCommand(full_name: string) { + return new Promise((resolve, reject) => { + const teaInstallCommand = new Command('tea-install', [`+${full_name}`, 'true']); + teaInstallCommand.on('error', reject); + + const handleLineOutput = async (line: string | { code: number }) => { + const c = await child; + if ( + (typeof line === 'string' && line.includes('installed:')) || + (typeof line !== 'string' && line?.code === 0) + ) { + c.kill(); + resolve(c.pid); + } else if (typeof line !== 'string' && line?.code === 1) { + reject(); + } + }; + + teaInstallCommand.stdout.on('data', handleLineOutput); + teaInstallCommand.stderr.on('data', handleLineOutput); + teaInstallCommand.on('close', (line: string) => { + console.log('command closed!'); + handleLineOutput(line || ''); + }); + teaInstallCommand.on('error', (line: string) => { + console.log('command error!', line); + handleLineOutput(line || ''); + }); + const child = teaInstallCommand.spawn(); + }); +} diff --git a/modules/gui/src/libs/stores.ts b/modules/gui/src/libs/stores.ts index e29fa29..afd7409 100644 --- a/modules/gui/src/libs/stores.ts +++ b/modules/gui/src/libs/stores.ts @@ -8,8 +8,6 @@ import { getPackages, getFeaturedPackages, getPackageReviews, getAllPosts } from import initAuthStore from './stores/auth'; import initNavStore from './stores/nav'; -export const backLink = writable('/'); - export const featuredPackages = writable([]); function initPackagesStore() { diff --git a/modules/gui/src/libs/types.ts b/modules/gui/src/libs/types.ts index dd9f8be..5636799 100644 --- a/modules/gui/src/libs/types.ts +++ b/modules/gui/src/libs/types.ts @@ -3,7 +3,7 @@ // please use the package @tea/ui/src/types.ts // things that go there are shared types/shapes like ie: Package -import type { Package } from '@tea/ui/types'; +import type { Package, Developer } from '@tea/ui/types'; export enum PackageStates { AVAILABLE, @@ -36,3 +36,9 @@ export enum AuthStatus { SUCCESS = 'SUCCESS', FAILED = 'FAILED' } + +export type DeviceAuth = { + status: AuthStatus; + user: Developer; + key: string; +}; diff --git a/modules/gui/src/libs/v1Client.ts b/modules/gui/src/libs/v1Client.ts new file mode 100644 index 0000000..2de6043 --- /dev/null +++ b/modules/gui/src/libs/v1Client.ts @@ -0,0 +1,40 @@ +import { getClient } from '@tauri-apps/api/http'; +import type { Session } from '$libs/stores/auth'; +import bcrypt from 'bcryptjs'; +import { getSession } from '$libs/stores/auth'; +import urlJoin from 'url-join'; + +export const baseUrl = 'https://api.tea.xyz/v1'; + +export async function get(path: string, query?: { [key: string]: string }) { + const [session, client] = await Promise.all([getSession(), getClient()]); + + const uri = urlJoin(baseUrl, path); + + const headers = + session?.device_id && session?.user + ? await getHeaders(`GET/${path}`, session) + : { Authorization: 'public ' }; + + const { data } = await client.get(uri.toString(), { + headers, + query: query || {} + }); + return data; +} + +async function getHeaders(path: string, session: Session) { + const unixMs = new Date().getTime(); + const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex + const deviceId = session.device_id?.split('-')[0]; + const preHash = [unixHexSecs, session.key, deviceId, path].join(''); + + const Authorization = bcrypt.hashSync(preHash, 10); + + return { + Authorization, + ['tea-ts']: unixMs.toString(), + ['tea-uid']: session.user?.developer_id, + ['tea-gui_id']: session.device_id + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f070d26..a04dac6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,7 @@ importers: '@tauri-apps/cli': ^1.2.2 '@tea/ui': workspace:* '@types/bcryptjs': ^2.4.2 + '@types/url-join': ^4.0.1 '@typescript-eslint/eslint-plugin': ^5.27.0 '@typescript-eslint/parser': ^5.27.0 autoprefixer: ^10.4.13 @@ -38,16 +39,19 @@ importers: tailwindcss: ^3.2.4 tslib: ^2.3.1 typescript: ^4.7.4 + url-join: ^5.0.0 vite: ^4.0.0 dependencies: '@tauri-apps/api': 1.2.0 '@types/bcryptjs': 2.4.2 + '@types/url-join': 4.0.1 bcryptjs: 2.4.3 buffer: 6.0.3 fuse.js: 6.6.2 lodash: 4.17.21 lorem-ipsum: 2.0.8 svelte-watch-resize: 1.0.3 + url-join: 5.0.0 devDependencies: '@playwright/test': 1.25.0 '@sveltejs/adapter-auto': 1.0.0_@sveltejs+kit@1.0.1 @@ -3896,6 +3900,10 @@ packages: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} dev: true + /@types/url-join/4.0.1: + resolution: {integrity: sha512-wDXw9LEEUHyV+7UWy7U315nrJGJ7p1BzaCxDpEoLr789Dk1WDVMMlf3iBfbG2F8NdWnYyFbtTxUn2ZNbm1Q4LQ==} + dev: false + /@types/webpack-env/1.18.0: resolution: {integrity: sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg==} dev: true @@ -10345,6 +10353,11 @@ packages: deprecated: Please see https://github.com/lydell/urix#deprecated dev: true + /url-join/5.0.0: + resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + /use/3.1.1: resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} engines: {node: '>=0.10.0'}