update electron android adb over browser
This commit is contained in:
parent
21109e4bf9
commit
f6295a9c2f
26 changed files with 1551 additions and 172 deletions
|
|
@ -34,7 +34,7 @@ We have some environment variables to control app behavior. The environment vari
|
||||||
|
|
||||||
## Environment Mode
|
## Environment Mode
|
||||||
|
|
||||||
In this Client project, we have two environment modes: `electron` and `web`. Business logic will be different in these two modes. you wrap the code in `if (import.meta.env.MODE === 'electron')` to run the code in electron mode. You can also use `if (import.meta.env.MODE === 'web')` to run the code in web mode.
|
In this Client project, we have two environment modes: `electron` and `web`. Business logic will be different in these two modes. you wrap the code in `if (window.electronRuntime)` to run the code in electron mode. You can also use `if (!window.electronRuntime)` to run the code in web mode.
|
||||||
|
|
||||||
The reason why we have two environment modes is that in electron we have to use native modules for example `deeplink` to handle login callback. But in web, we have to use `window.location.href` to handle login callback.
|
The reason why we have two environment modes is that in electron we have to use native modules for example `deeplink` to handle login callback. But in web, we have to use `window.location.href` to handle login callback.
|
||||||
|
|
||||||
|
|
|
||||||
19
client-electron/electron/adb/adbDaemonUSB.ts
Normal file
19
client-electron/electron/adb/adbDaemonUSB.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// import type { AdbDaemonWebUsbDevice } from '@yume-chan/adb-daemon-webusb'
|
||||||
|
// import { AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb'
|
||||||
|
import { webusb } from 'usb'
|
||||||
|
|
||||||
|
// const WebUsb: WebUSB = new WebUSB({ allowAllDevices: true })
|
||||||
|
// const Manager: AdbDaemonWebUsbDeviceManager = new AdbDaemonWebUsbDeviceManager(
|
||||||
|
// WebUsb
|
||||||
|
// )
|
||||||
|
|
||||||
|
export function getDevices() {
|
||||||
|
// const devices: AdbDaemonWebUsbDevice[] = await Manager.getDevices()
|
||||||
|
// if (!devices.length) {
|
||||||
|
// alert('No device connected')
|
||||||
|
// }
|
||||||
|
|
||||||
|
webusb.requestDevice({ filters: [] }).then(device => {
|
||||||
|
console.log(device)
|
||||||
|
})
|
||||||
|
}
|
||||||
34
client-electron/electron/deeplink.ts
Normal file
34
client-electron/electron/deeplink.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { type BrowserWindow } from 'electron'
|
||||||
|
|
||||||
|
export default function (
|
||||||
|
app: Electron.App,
|
||||||
|
win: BrowserWindow | null,
|
||||||
|
ipcMain: Electron.IpcMain,
|
||||||
|
shell: Electron.Shell
|
||||||
|
) {
|
||||||
|
ipcMain.on('deeplink', (_event, url) => {
|
||||||
|
// open browser
|
||||||
|
shell.openExternal(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('open-url', (_event, url) => {
|
||||||
|
const paramsString = url.split('://')[1]
|
||||||
|
|
||||||
|
const kind = paramsString.split('?')[0]
|
||||||
|
|
||||||
|
const params = new URLSearchParams(paramsString)
|
||||||
|
|
||||||
|
if (kind === '/login') {
|
||||||
|
win?.webContents.send('loginSuccess', {
|
||||||
|
id: params.get('id'),
|
||||||
|
name: params.get('name'),
|
||||||
|
email: params.get('email'),
|
||||||
|
picture: params.get('picture'),
|
||||||
|
permissions: params.get('permissions'),
|
||||||
|
access_token: params.get('access_token'),
|
||||||
|
max_age: params.get('max_age'),
|
||||||
|
refresh_token: params.get('refresh_token')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
2
client-electron/electron/electron-env.d.ts
vendored
2
client-electron/electron/electron-env.d.ts
vendored
|
|
@ -25,4 +25,6 @@ declare namespace NodeJS {
|
||||||
// Used in Renderer process, expose in `preload.ts`
|
// Used in Renderer process, expose in `preload.ts`
|
||||||
interface Window {
|
interface Window {
|
||||||
ipcRenderer: import('electron').IpcRenderer
|
ipcRenderer: import('electron').IpcRenderer
|
||||||
|
electronRuntime: boolean
|
||||||
|
platform: NodeJS.Platform
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
client-electron/electron/keychain.ts
Normal file
22
client-electron/electron/keychain.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { findCredentials, getPassword, setPassword } from '@postman/node-keytar'
|
||||||
|
|
||||||
|
export function eventGetKeyChain(icpMain: Electron.IpcMain) {
|
||||||
|
icpMain.on('get-keyChain', (event, serviceName, account) => {
|
||||||
|
getPassword(serviceName, account).then(password => {
|
||||||
|
event.returnValue = password
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
icpMain.on('set-keyChain', (_event, serviceName, account, password) => {
|
||||||
|
setPassword(serviceName, account, password)
|
||||||
|
})
|
||||||
|
|
||||||
|
icpMain.on('delete-keyChain', (_event, serviceName, account) => {
|
||||||
|
setPassword(serviceName, account, '')
|
||||||
|
})
|
||||||
|
|
||||||
|
icpMain.handle('keyChainSync', async (_event, serviceName) => {
|
||||||
|
const credentials = await findCredentials(serviceName)
|
||||||
|
return credentials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { app, BrowserWindow, ipcMain, shell } from 'electron'
|
import { app, BrowserWindow, ipcMain, shell } from 'electron'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
import deeplink from './deeplink'
|
||||||
|
import { eventGetKeyChain } from './keychain'
|
||||||
|
|
||||||
// The built directory structure
|
// The built directory structure
|
||||||
//
|
//
|
||||||
|
|
@ -10,7 +12,7 @@ import path from 'node:path'
|
||||||
// │ │ ├── main.js
|
// │ │ ├── main.js
|
||||||
// │ │ └── preload.js
|
// │ │ └── preload.js
|
||||||
// │
|
// │
|
||||||
process.env.DIST = path.join(__dirname, '../dist')
|
process.env.DIST = path.join(__dirname, '../dist-renderer')
|
||||||
process.env.VITE_PUBLIC = app.isPackaged
|
process.env.VITE_PUBLIC = app.isPackaged
|
||||||
? process.env.DIST
|
? process.env.DIST
|
||||||
: path.join(process.env.DIST, '../public')
|
: path.join(process.env.DIST, '../public')
|
||||||
|
|
@ -42,6 +44,61 @@ function createWindow() {
|
||||||
app.setAsDefaultProtocolClient('taobin-electron')
|
app.setAsDefaultProtocolClient('taobin-electron')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let grantedDeviceThroughPermHandler: Electron.USBDevice
|
||||||
|
|
||||||
|
win.webContents.session.on(
|
||||||
|
'select-usb-device',
|
||||||
|
(event, details, callback) => {
|
||||||
|
// Add events to handle devices being added or removed before the callback on
|
||||||
|
// `select-usb-device` is called.
|
||||||
|
win?.webContents.session.on('usb-device-added', (_event, device) => {
|
||||||
|
console.log('usb-device-added FIRED WITH', device)
|
||||||
|
// Optionally update details.deviceList
|
||||||
|
})
|
||||||
|
|
||||||
|
win?.webContents.session.on('usb-device-removed', (_event, device) => {
|
||||||
|
console.log('usb-device-removed FIRED WITH', device)
|
||||||
|
// Optionally update details.deviceList
|
||||||
|
})
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
if (details.deviceList && details.deviceList.length > 0) {
|
||||||
|
const deviceToReturn = details.deviceList.find(
|
||||||
|
device => device.vendorId === 1478
|
||||||
|
)
|
||||||
|
if (deviceToReturn) {
|
||||||
|
callback(deviceToReturn.deviceId)
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
win.webContents.session.setPermissionCheckHandler(
|
||||||
|
(_webContents, permission) => {
|
||||||
|
if (permission === 'usb') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
win.webContents.session.setDevicePermissionHandler(details => {
|
||||||
|
console.log(details)
|
||||||
|
if (details.deviceType === 'usb' && details.device.vendorId == 1478) {
|
||||||
|
if (!grantedDeviceThroughPermHandler) {
|
||||||
|
grantedDeviceThroughPermHandler = details.device as Electron.USBDevice
|
||||||
|
return true
|
||||||
|
} else if (grantedDeviceThroughPermHandler.vendorId === 1478) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
if (VITE_DEV_SERVER_URL) {
|
if (VITE_DEV_SERVER_URL) {
|
||||||
win.loadURL(VITE_DEV_SERVER_URL)
|
win.loadURL(VITE_DEV_SERVER_URL)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -68,17 +125,12 @@ app.on('activate', () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('open-url', (_event, url) => {
|
// Create MainWindow, load the rest of the app, etc...
|
||||||
win?.webContents.send('deeplink', url)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create mainWindow, load the rest of the app, etc...
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow()
|
createWindow()
|
||||||
})
|
// deeplink
|
||||||
|
deeplink(app, win, ipcMain, shell)
|
||||||
|
|
||||||
// deeplink
|
//keychain
|
||||||
ipcMain.on('deeplink', (_event, url) => {
|
eventGetKeyChain(ipcMain)
|
||||||
// open browser
|
|
||||||
shell.openExternal(url)
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import { contextBridge, ipcRenderer } from 'electron'
|
||||||
|
|
||||||
// --------- Expose some API to the Renderer process ---------
|
// --------- Expose some API to the Renderer process ---------
|
||||||
contextBridge.exposeInMainWorld('ipcRenderer', withPrototype(ipcRenderer))
|
contextBridge.exposeInMainWorld('ipcRenderer', withPrototype(ipcRenderer))
|
||||||
|
contextBridge.exposeInMainWorld('electronRuntime', true)
|
||||||
|
contextBridge.exposeInMainWorld('platform', process.platform)
|
||||||
|
|
||||||
// `exposeInMainWorld` can't detect attributes and methods of `prototype`, manually patching it.
|
// `exposeInMainWorld` can't detect attributes and methods of `prototype`, manually patching it.
|
||||||
function withPrototype(obj: Record<string, any>) {
|
function withPrototype(obj: Record<string, any>) {
|
||||||
|
|
|
||||||
924
client-electron/package-lock.json
generated
924
client-electron/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -2,24 +2,42 @@
|
||||||
"name": "client-electron",
|
"name": "client-electron",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
"author": {
|
||||||
|
"name": "Kenta420",
|
||||||
|
"email": "poomipat.c@forth.ac.th"
|
||||||
|
},
|
||||||
|
"description": "An Application for Taobin to manage recipes and ingredients",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev:electron\" \"npm run dev:web\"",
|
"dev": "concurrently \"APP_KIND=electron npm run dev:electron\" \"APP_KIND=web npm run dev:web\"",
|
||||||
"dev:electron": "vite -c vite.config.electron.ts",
|
"dev:electron": "vite -c vite.config.electron.ts",
|
||||||
"dev:web": "vite -c vite.config.web.ts --open",
|
"dev:web": "vite -c vite.config.web.ts --open",
|
||||||
"build": "concurrently \"npm run build:electron\" \"npm run build:web\"",
|
"build": "concurrently \"npm run build:electron\" \"npm run build:web\"",
|
||||||
"build:electron": "tsc && vite build -c vite.config.electron.ts --mode=production && electron-builder",
|
"build:electron": "npm run prebuild:electron && tsc && vite build -c vite.config.electron.ts --mode=production && electron-builder",
|
||||||
"build:web": "tsc && vite build -c vite.config.web.ts --mode=production",
|
"build:web": "npm run prebuild:web && tsc && vite build -c vite.config.web.ts --mode=production",
|
||||||
|
"prebuild:electron": "rm -rf dist-renderer && rm -rf dist-electron && vite optimize -c vite.config.electron.ts -m production",
|
||||||
|
"prebuild:web": "rm -rf dist-web && vite optimize -c vite.config.web.ts -m production",
|
||||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@postman/node-keytar": "^7.9.3",
|
||||||
|
"@yume-chan/adb": "^0.0.22",
|
||||||
|
"@yume-chan/adb-credential-web": "^0.0.22",
|
||||||
|
"@yume-chan/adb-daemon-webusb": "^0.0.22",
|
||||||
|
"@yume-chan/adb-scrcpy": "^0.0.22",
|
||||||
|
"@yume-chan/scrcpy": "^0.0.22",
|
||||||
|
"@yume-chan/scrcpy-decoder-webcodecs": "^0.0.22",
|
||||||
|
"@yume-chan/stream-extra": "^0.0.22",
|
||||||
|
"axios": "^1.6.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.21.1",
|
"react-router-dom": "^6.21.1",
|
||||||
|
"usb": "^2.11.0",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.2.0",
|
"@electron-forge/cli": "^7.2.0",
|
||||||
|
"@electron/rebuild": "^3.5.0",
|
||||||
"@types/react": "^18.2.21",
|
"@types/react": "^18.2.21",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||||
|
|
|
||||||
|
|
@ -6,26 +6,36 @@ import {
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import AuthCallBack from './AuthCallBack'
|
import AuthCallBack from './AuthCallBack'
|
||||||
import MainLayout from './layouts/MainLayout'
|
import MainLayout from './layouts/MainLayout'
|
||||||
import Home from './pages/Home'
|
import HomePage from './pages/Home'
|
||||||
|
import LoginPage from './pages/Login'
|
||||||
|
import AndroidPage from './pages/Android'
|
||||||
|
|
||||||
function router() {
|
function router() {
|
||||||
const routes: RouteObject[] = [
|
const routes: RouteObject[] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: (
|
element: <MainLayout />,
|
||||||
<MainLayout>
|
|
||||||
<Home />
|
|
||||||
</MainLayout>
|
|
||||||
),
|
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <HomePage />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'recipes',
|
path: 'recipes',
|
||||||
element: <div>Recipes</div>
|
element: <div>Recipes</div>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'android',
|
||||||
|
element: <AndroidPage />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/auth/callback',
|
path: '/login',
|
||||||
|
element: <LoginPage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/callback',
|
||||||
element: <AuthCallBack />
|
element: <AuthCallBack />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -38,8 +48,7 @@ function router() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
if (window.electronRuntime) {
|
||||||
if (import.meta.env.MODE == 'electron') {
|
|
||||||
return createHashRouter(routes)
|
return createHashRouter(routes)
|
||||||
}
|
}
|
||||||
return createBrowserRouter(routes)
|
return createBrowserRouter(routes)
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,24 @@
|
||||||
// then emit a message to the main process to close the window
|
// then emit a message to the main process to close the window
|
||||||
const AuthCallBack: React.FC = () => {
|
const AuthCallBack: React.FC = () => {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
// emit message to main process
|
// emit message to main process
|
||||||
|
|
||||||
window.opener.postMessage(
|
window.opener.postMessage(
|
||||||
{
|
{
|
||||||
payload: 'loginSuccess',
|
payload: 'loginSuccess',
|
||||||
data: params
|
data: {
|
||||||
|
id: params.get('id'),
|
||||||
|
name: params.get('name'),
|
||||||
|
email: params.get('email'),
|
||||||
|
picture: params.get('picture'),
|
||||||
|
permissions: params.get('permissions')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'*'
|
'*'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
window.close()
|
||||||
|
|
||||||
return <div>it just call back</div>
|
return <div>it just call back</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
BIN
client-electron/src/bin/scrcpy-server.bin
Normal file
BIN
client-electron/src/bin/scrcpy-server.bin
Normal file
Binary file not shown.
19
client-electron/src/hooks/adb.ts
Normal file
19
client-electron/src/hooks/adb.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import {
|
||||||
|
type AdbDaemonWebUsbDevice,
|
||||||
|
AdbDaemonWebUsbDeviceManager
|
||||||
|
} from '@yume-chan/adb-daemon-webusb'
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
interface ADB {
|
||||||
|
manager: AdbDaemonWebUsbDeviceManager | undefined
|
||||||
|
device: AdbDaemonWebUsbDevice | undefined
|
||||||
|
setDevice: (device: AdbDaemonWebUsbDevice | undefined) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useAdb = create<ADB>(set => ({
|
||||||
|
manager: AdbDaemonWebUsbDeviceManager.BROWSER,
|
||||||
|
device: undefined,
|
||||||
|
setDevice: device => set({ device })
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default useAdb
|
||||||
|
|
@ -1,14 +1,40 @@
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
|
import customAxios from '../utils/customAxios'
|
||||||
|
|
||||||
interface UserAuth {
|
export interface UserInfo {
|
||||||
username: string | undefined
|
id: string
|
||||||
email: string | undefined
|
name: string
|
||||||
|
email: string
|
||||||
|
picture: string
|
||||||
|
permissions: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const userAuthStore = create<UserAuth>(() => ({
|
interface UserAuth {
|
||||||
username: undefined,
|
userInfo: UserInfo | null
|
||||||
email: undefined
|
setUserInfo: (userInfo: UserInfo | null) => void
|
||||||
|
getUserInfo: () => Promise<UserInfo | null>
|
||||||
|
logout: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAuthStore = create<UserAuth>(set => ({
|
||||||
|
userInfo: null,
|
||||||
|
setUserInfo: userInfo => set({ userInfo }),
|
||||||
|
getUserInfo: () => {
|
||||||
|
return customAxios
|
||||||
|
.get<UserInfo>('/user/me')
|
||||||
|
.then(res => {
|
||||||
|
set({ userInfo: res.data })
|
||||||
|
return res.data
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
set({ userInfo: null })
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
},
|
||||||
|
logout: () => {
|
||||||
|
customAxios.post('/auth/logout')
|
||||||
|
set({ userInfo: null })
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const getUserName = userAuthStore(state => state.username)
|
export default userAuthStore
|
||||||
export const getUserEmail = userAuthStore(state => state.email)
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,41 @@
|
||||||
|
import { Outlet, useNavigate } from 'react-router-dom'
|
||||||
import Header from '../components/Header'
|
import Header from '../components/Header'
|
||||||
import Sidebar from '../components/Sidebar'
|
import Sidebar from '../components/Sidebar'
|
||||||
|
import userAuthStore from '../hooks/userAuth'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
const MainLayout = () => {
|
||||||
|
const { userInfo, getUserInfo } = userAuthStore(
|
||||||
|
useShallow(state => ({
|
||||||
|
userInfo: state.userInfo,
|
||||||
|
getUserInfo: state.getUserInfo
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
// get current path
|
||||||
|
const currentPath = window.location.pathname
|
||||||
|
|
||||||
|
useCallback(() => {
|
||||||
|
console.log(import.meta.env.NODE_ENV)
|
||||||
|
if (!userInfo && import.meta.env.NODE_ENV !== 'development') {
|
||||||
|
getUserInfo().then(userInfo => {
|
||||||
|
// if still not login then redirect to login page
|
||||||
|
if (!userInfo) {
|
||||||
|
navigate('/login?redirect=' + currentPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [userInfo, getUserInfo, navigate, currentPath])
|
||||||
|
|
||||||
const MainLayout = ({ children }: React.PropsWithChildren) => {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<Header />
|
<Header />
|
||||||
<main className="fixed pt-14 px-4 pl-[21rem]">{children}</main>
|
<main className="fixed pt-14 px-4 pl-[21rem]">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log(import.meta.env.MODE)
|
if (window.electronRuntime) {
|
||||||
|
|
||||||
if (import.meta.env.MODE === 'electron') {
|
|
||||||
// Remove Preload scripts loading
|
// Remove Preload scripts loading
|
||||||
postMessage({ payload: 'removeLoading' }, '*')
|
postMessage({ payload: 'removeLoading' }, '*')
|
||||||
|
|
||||||
|
|
@ -20,6 +18,41 @@ if (import.meta.env.MODE === 'electron') {
|
||||||
console.log(message)
|
console.log(message)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
window.ipcRenderer
|
||||||
|
.invoke(
|
||||||
|
'keyChainSync',
|
||||||
|
import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME
|
||||||
|
)
|
||||||
|
.then(result => {
|
||||||
|
console.log(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
// getPassword(
|
||||||
|
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||||
|
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN
|
||||||
|
// ).then(tokenMaxAge => {
|
||||||
|
// if (tokenMaxAge) {
|
||||||
|
// const [token, max_age] = tokenMaxAge.split(';')
|
||||||
|
// document.cookie =
|
||||||
|
// 'access_token=' +
|
||||||
|
// token +
|
||||||
|
// '; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=' +
|
||||||
|
// max_age
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// getPassword(
|
||||||
|
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||||
|
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN
|
||||||
|
// ).then(refreshToken => {
|
||||||
|
// if (refreshToken) {
|
||||||
|
// document.cookie =
|
||||||
|
// 'refresh_token=' +
|
||||||
|
// refreshToken +
|
||||||
|
// '; Path=/; HttpOnly; SameSite=None; Secure;'
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
// Use deep link
|
// Use deep link
|
||||||
window.ipcRenderer.on('deeplink', (_event, url) => {
|
window.ipcRenderer.on('deeplink', (_event, url) => {
|
||||||
console.log(url)
|
console.log(url)
|
||||||
|
|
|
||||||
178
client-electron/src/pages/Android.tsx
Normal file
178
client-electron/src/pages/Android.tsx
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
import { ADB_DEFAULT_DEVICE_FILTER } from '@yume-chan/adb-daemon-webusb'
|
||||||
|
import AdbWebCredentialStore from '@yume-chan/adb-credential-web'
|
||||||
|
import { Adb, AdbDaemonTransport } from '@yume-chan/adb'
|
||||||
|
import useAdb from '../hooks/adb'
|
||||||
|
import type { AdbScrcpyVideoStream } from '@yume-chan/adb-scrcpy'
|
||||||
|
import { AdbScrcpyClient, AdbScrcpyOptions2_1 } from '@yume-chan/adb-scrcpy'
|
||||||
|
import { WebCodecsDecoder } from '@yume-chan/scrcpy-decoder-webcodecs'
|
||||||
|
import {
|
||||||
|
ScrcpyLogLevel1_18,
|
||||||
|
ScrcpyOptions2_1,
|
||||||
|
ScrcpyVideoCodecId
|
||||||
|
} from '@yume-chan/scrcpy'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
const ScreenStream: React.FC<{ child: HTMLCanvasElement }> = ({
|
||||||
|
child
|
||||||
|
}: {
|
||||||
|
child: HTMLCanvasElement
|
||||||
|
}) => {
|
||||||
|
return <div ref={ref => ref?.appendChild(child)}></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const AndroidPage: React.FC = () => {
|
||||||
|
const { manager, device, setDevice } = useAdb(
|
||||||
|
useShallow(state => ({
|
||||||
|
manager: state.manager,
|
||||||
|
device: state.device,
|
||||||
|
setDevice: state.setDevice
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const [decoder, setDecoder] = useState<WebCodecsDecoder | undefined>()
|
||||||
|
const [client, setClient] = useState<AdbScrcpyClient | undefined>()
|
||||||
|
const [adbClient, setAdbClient] = useState<Adb | undefined>(undefined)
|
||||||
|
|
||||||
|
function attachDevice() {
|
||||||
|
manager
|
||||||
|
?.requestDevice({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
...ADB_DEFAULT_DEVICE_FILTER,
|
||||||
|
vendorId: 1478
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then(selectedDevice => {
|
||||||
|
if (!selectedDevice) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
setDevice(selectedDevice)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function disConnectDevice() {
|
||||||
|
device?.raw.forget()
|
||||||
|
setDevice(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
// function reboot() {
|
||||||
|
// device?.connect().then(connection => {
|
||||||
|
// const credentialStore: AdbWebCredentialStore = new AdbWebCredentialStore()
|
||||||
|
|
||||||
|
// AdbDaemonTransport.authenticate({
|
||||||
|
// serial: device?.serial,
|
||||||
|
// connection,
|
||||||
|
// credentialStore: credentialStore
|
||||||
|
// }).then(transport => {
|
||||||
|
// const adb: Adb = new Adb(transport)
|
||||||
|
|
||||||
|
// adb.power.reboot().then(() => {
|
||||||
|
// console.log('reboot success')
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
async function scrcpyConnect() {
|
||||||
|
// const url = new URL('../bin/scrcpy-server.bin', import.meta.url)
|
||||||
|
// const server: ArrayBuffer = await fetch(url).then(res => res.arrayBuffer())
|
||||||
|
|
||||||
|
const connection = await device?.connect()
|
||||||
|
|
||||||
|
const credentialStore: AdbWebCredentialStore = new AdbWebCredentialStore()
|
||||||
|
|
||||||
|
const transport = await AdbDaemonTransport.authenticate({
|
||||||
|
serial: device!.serial,
|
||||||
|
connection: connection!,
|
||||||
|
credentialStore: credentialStore
|
||||||
|
})
|
||||||
|
|
||||||
|
const adb: Adb = new Adb(transport)
|
||||||
|
setAdbClient(adb)
|
||||||
|
// await AdbScrcpyClient.pushServer(
|
||||||
|
// adb,
|
||||||
|
// new ReadableStream({
|
||||||
|
// start(controller) {
|
||||||
|
// controller.enqueue(new Consumable(new Uint8Array(server)))
|
||||||
|
// controller.close()
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
|
||||||
|
await adb.subprocess.spawn(
|
||||||
|
'CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 2.1'
|
||||||
|
)
|
||||||
|
|
||||||
|
const scrcpyOption = new ScrcpyOptions2_1({
|
||||||
|
audio: false,
|
||||||
|
maxFps: 60,
|
||||||
|
control: false,
|
||||||
|
video: true,
|
||||||
|
logLevel: ScrcpyLogLevel1_18.Debug,
|
||||||
|
videoCodec: 'h264',
|
||||||
|
stayAwake: true,
|
||||||
|
cleanup: true
|
||||||
|
})
|
||||||
|
const _client = await AdbScrcpyClient.start(
|
||||||
|
adb,
|
||||||
|
'/data/local/tmp/scrcpy-server.jar',
|
||||||
|
'2.1',
|
||||||
|
new AdbScrcpyOptions2_1(scrcpyOption)
|
||||||
|
)
|
||||||
|
|
||||||
|
setClient(_client)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scrcpyStream() {
|
||||||
|
const videoStream: AdbScrcpyVideoStream | undefined =
|
||||||
|
await client?.videoStream
|
||||||
|
const _decoder = new WebCodecsDecoder(ScrcpyVideoCodecId.H264)
|
||||||
|
videoStream?.stream.pipeTo(_decoder.writable)
|
||||||
|
setDecoder(_decoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrcpyDisconnect() {
|
||||||
|
client?.close()
|
||||||
|
setClient(undefined)
|
||||||
|
setDecoder(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebootDevice() {
|
||||||
|
adbClient?.power.reboot()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>This is Android Page!!!!!!!</h1>
|
||||||
|
{device ? <h2>Device: {device.name}</h2> : <h2>No Device</h2>}
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<button className="btn btn-primary" onClick={attachDevice}>
|
||||||
|
Attach Device
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-primary" onClick={disConnectDevice}>
|
||||||
|
Disconnect Device
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-primary" onClick={rebootDevice}>
|
||||||
|
Reboot Device
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<button className="btn btn-primary" onClick={scrcpyConnect}>
|
||||||
|
Scrcpy Connect
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-primary" onClick={scrcpyStream}>
|
||||||
|
Scrcpy Stream
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-primary" onClick={scrcpyDisconnect}>
|
||||||
|
Scrcpy Disconnect
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{decoder ? <ScreenStream child={decoder.renderer} /> : ''}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AndroidPage
|
||||||
|
|
@ -1,51 +1,15 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import googleLogo from '../assets/google-color.svg'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
const Home: React.FC = () => {
|
|
||||||
console.log(import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL)
|
|
||||||
|
|
||||||
const loginWithGoogle = () => {
|
|
||||||
// if is web mode then use window.open
|
|
||||||
// if is electron mode then use ipcRenderer.send to use deep link method
|
|
||||||
if (import.meta.env.MODE === 'web') {
|
|
||||||
// open new window and listen to message from window.opener
|
|
||||||
const newWindow = window.open(
|
|
||||||
import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL,
|
|
||||||
'_blank',
|
|
||||||
'width=500,height=600'
|
|
||||||
)
|
|
||||||
|
|
||||||
// listen to message from new window
|
|
||||||
window.addEventListener('message', event => {
|
|
||||||
if (event.data.payload === 'loginSuccess') {
|
|
||||||
// close new window
|
|
||||||
newWindow?.close()
|
|
||||||
|
|
||||||
console.log(event.data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
window.ipcRenderer.send('deeplink', 'http://127.0.0.1:5500/test.html')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const HomePage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>This is Home Page!!!!!!!</h1>
|
<h1>This is Home Page!!!!!!!</h1>
|
||||||
<button
|
<button className="btn btn-primary">
|
||||||
onClick={loginWithGoogle}
|
<Link to="/android">Go to Android Page</Link>
|
||||||
className="bg-white px-4 py-2 border flex gap-2 border-slate-200 rounded-lg text-slate-700 hover:border-slate-400 hover:text-slate-900 hover:shadow transition duration-150"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className="w-6 h-6"
|
|
||||||
src={googleLogo}
|
|
||||||
alt="google logo"
|
|
||||||
loading="eager"
|
|
||||||
/>
|
|
||||||
<span>Login with @forth.co.th Google account</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home
|
export default HomePage
|
||||||
|
|
|
||||||
85
client-electron/src/pages/Login.tsx
Normal file
85
client-electron/src/pages/Login.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import googleLogo from '../assets/google-color.svg'
|
||||||
|
import userAuthStore, { type UserInfo } from '../hooks/userAuth'
|
||||||
|
|
||||||
|
const LoginPage: React.FC = () => {
|
||||||
|
const setUserInfo = userAuthStore(state => state.setUserInfo)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const redirectUrl =
|
||||||
|
new URLSearchParams(window.location.search).get('redirect') ?? '/'
|
||||||
|
|
||||||
|
const loginWithGoogle = () => {
|
||||||
|
// if is web mode then use window.open
|
||||||
|
// if is electron mode then use ipcRenderer.send to use deep link method
|
||||||
|
if (window.electronRuntime) {
|
||||||
|
window.ipcRenderer.send(
|
||||||
|
'deeplink',
|
||||||
|
import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL +
|
||||||
|
'/auth/google?redirect_to=' +
|
||||||
|
redirectUrl +
|
||||||
|
'&kind=electron'
|
||||||
|
)
|
||||||
|
|
||||||
|
window.ipcRenderer.on('loginSuccess', (_event, data) => {
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
setUserInfo(data satisfies UserInfo)
|
||||||
|
navigate(redirectUrl)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// open new window and listen to message from window.opener
|
||||||
|
window.open(
|
||||||
|
import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL +
|
||||||
|
'/auth/google?redirect_to=' +
|
||||||
|
redirectUrl,
|
||||||
|
'_blank',
|
||||||
|
'width=500,height=600'
|
||||||
|
)
|
||||||
|
|
||||||
|
// listen to message from new window
|
||||||
|
window.addEventListener('message', event => {
|
||||||
|
if (event.data.payload === 'loginSuccess') {
|
||||||
|
// const { access_token, max_age, refresh_token } = event.data.data
|
||||||
|
|
||||||
|
// setPassword(
|
||||||
|
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||||
|
// import.meta.env
|
||||||
|
// .TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
|
||||||
|
// access_token + ';' + max_age
|
||||||
|
// )
|
||||||
|
|
||||||
|
// setPassword(
|
||||||
|
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||||
|
// import.meta.env
|
||||||
|
// .TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
|
||||||
|
// refresh_token
|
||||||
|
// )
|
||||||
|
|
||||||
|
setUserInfo(event.data.data satisfies UserInfo)
|
||||||
|
|
||||||
|
navigate(redirectUrl)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-screen">
|
||||||
|
<button
|
||||||
|
onClick={loginWithGoogle}
|
||||||
|
className="bg-white px-4 py-2 border flex gap-2 border-slate-200 rounded-lg text-slate-700 hover:border-slate-400 hover:text-slate-900 hover:shadow transition duration-150"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="w-6 h-6"
|
||||||
|
src={googleLogo}
|
||||||
|
alt="google logo"
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
<span>Login with @forth.co.th Google account</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage
|
||||||
14
client-electron/src/services/usb.service.ts
Normal file
14
client-electron/src/services/usb.service.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb'
|
||||||
|
|
||||||
|
const Manager: AdbDaemonWebUsbDeviceManager | undefined =
|
||||||
|
AdbDaemonWebUsbDeviceManager.BROWSER
|
||||||
|
|
||||||
|
export async function getDevices() {
|
||||||
|
if (!Manager) {
|
||||||
|
alert('WebUSB is not supported in this browser')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const devices = await Manager.getDevices()
|
||||||
|
return devices
|
||||||
|
}
|
||||||
26
client-electron/src/utils/customAxios.ts
Normal file
26
client-electron/src/utils/customAxios.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const customAxios = axios.create({
|
||||||
|
baseURL: import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL,
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
customAxios.interceptors.response.use(
|
||||||
|
res => res,
|
||||||
|
err => {
|
||||||
|
const originalRequest = err.config
|
||||||
|
|
||||||
|
if (err.response.status === 401 && !originalRequest._retry) {
|
||||||
|
originalRequest._retry = true
|
||||||
|
return customAxios.get('/auth/refresh').then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
return customAxios(originalRequest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default customAxios
|
||||||
8
client-electron/src/vite-env.d.ts
vendored
8
client-electron/src/vite-env.d.ts
vendored
|
|
@ -1 +1,9 @@
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
TAOBIN_RECIPE_MANAGER_SERVER_URL: string
|
||||||
|
TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME: string
|
||||||
|
TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN: string
|
||||||
|
TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN: string
|
||||||
|
MAIN_VITE_TEST: boolean
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,20 @@ import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
mode: 'electron',
|
|
||||||
envPrefix: 'TAOBIN_RECIPE_MANAGER_',
|
envPrefix: 'TAOBIN_RECIPE_MANAGER_',
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
electron({
|
electron({
|
||||||
main: {
|
main: {
|
||||||
// Shortcut of `build.lib.entry`.
|
// Shortcut of `build.lib.entry`.
|
||||||
entry: 'electron/main.ts'
|
entry: 'electron/main.ts',
|
||||||
|
vite: {
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['@postman/node-keytar']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
// Shortcut of `build.rollupOptions.input`.
|
// Shortcut of `build.rollupOptions.input`.
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,14 @@ import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
mode: 'web',
|
|
||||||
envPrefix: 'TAOBIN_RECIPE_MANAGER_',
|
envPrefix: 'TAOBIN_RECIPE_MANAGER_',
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist-web'
|
outDir: 'dist-web'
|
||||||
},
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['node-keytar']
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 4200
|
port: 4200
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
ServerPort uint `mapstructure:"SERVER_PORT"`
|
ServerPort uint `mapstructure:"SERVER_PORT"`
|
||||||
AllowedOrigins string `mapstructure:"ALLOWED_ORIGINS"`
|
AllowedOrigins string `mapstructure:"ALLOWED_ORIGINS"`
|
||||||
ClientRedirectURL string `mapstructure:"CLIENT_REDIRECT_URL"`
|
ClientRedirectURL string `mapstructure:"CLIENT_REDIRECT_URL"`
|
||||||
ServerDomain string `mapstructure:"SERVER_DOMAIN"`
|
ClientElectronRedirectURL string `mapstructure:"CLIENT_ELECTRON_REDIRECT_URL"`
|
||||||
APIKey string `mapstructure:"API_KEY"`
|
ServerDomain string `mapstructure:"SERVER_DOMAIN"`
|
||||||
Debug bool `mapstructure:"DEBUG_MODE"`
|
APIKey string `mapstructure:"API_KEY"`
|
||||||
|
Debug bool `mapstructure:"DEBUG_MODE"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"github.com/go-chi/chi/v5"
|
"encoding/json"
|
||||||
"go.uber.org/zap"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"recipe-manager/config"
|
"recipe-manager/config"
|
||||||
|
|
@ -15,6 +13,10 @@ import (
|
||||||
"recipe-manager/services/user"
|
"recipe-manager/services/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthRouter struct {
|
type AuthRouter struct {
|
||||||
|
|
@ -42,6 +44,10 @@ func (ar *AuthRouter) Route(r chi.Router) {
|
||||||
stateMap["redirect_to"] = r.URL.Query().Get("redirect_to")
|
stateMap["redirect_to"] = r.URL.Query().Get("redirect_to")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.URL.Query().Get("kind") != "" {
|
||||||
|
stateMap["kind"] = r.URL.Query().Get("kind")
|
||||||
|
}
|
||||||
|
|
||||||
authURL := ar.oauth.AuthURL(state, stateMap)
|
authURL := ar.oauth.AuthURL(state, stateMap)
|
||||||
http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
|
http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
|
||||||
})
|
})
|
||||||
|
|
@ -52,6 +58,7 @@ func (ar *AuthRouter) Route(r chi.Router) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var redirectTo string
|
var redirectTo string
|
||||||
|
var kind string
|
||||||
state := r.URL.Query().Get("state")
|
state := r.URL.Query().Get("state")
|
||||||
if state == "" {
|
if state == "" {
|
||||||
http.Error(w, "State not found", http.StatusBadRequest)
|
http.Error(w, "State not found", http.StatusBadRequest)
|
||||||
|
|
@ -65,6 +72,7 @@ func (ar *AuthRouter) Route(r chi.Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectTo = val["redirect_to"]
|
redirectTo = val["redirect_to"]
|
||||||
|
kind = val["kind"]
|
||||||
|
|
||||||
ar.oauth.RemoveState(state)
|
ar.oauth.RemoveState(state)
|
||||||
}
|
}
|
||||||
|
|
@ -115,12 +123,62 @@ func (ar *AuthRouter) Route(r chi.Router) {
|
||||||
|
|
||||||
ar.taoLogger.Log.Info("User Log-In Success", zap.String("userInfo", userInfo.Name), zap.String("email", userInfo.Email))
|
ar.taoLogger.Log.Info("User Log-In Success", zap.String("userInfo", userInfo.Name), zap.String("email", userInfo.Email))
|
||||||
|
|
||||||
|
if kind == "electron" {
|
||||||
|
value.Add("access_token", token.AccessToken)
|
||||||
|
value.Add("max_age", "3600")
|
||||||
|
value.Add("refresh_token", token.RefreshToken)
|
||||||
|
http.Redirect(w, r, ar.cfg.ClientElectronRedirectURL+"login?"+value.Encode(), http.StatusTemporaryRedirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
// redirect to frontend with token and refresh token
|
// redirect to frontend with token and refresh token
|
||||||
w.Header().Add("set-cookie", "access_token="+token.AccessToken+"; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=3600")
|
w.Header().Add("set-cookie", "access_token="+token.AccessToken+"; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=3600")
|
||||||
w.Header().Add("set-cookie", "refresh_token="+token.RefreshToken+"; Path=/; HttpOnly; SameSite=None; Secure")
|
w.Header().Add("set-cookie", "refresh_token="+token.RefreshToken+"; Path=/; HttpOnly; SameSite=None; Secure")
|
||||||
http.Redirect(w, r, ar.cfg.ClientRedirectURL+"/?"+value.Encode(), http.StatusTemporaryRedirect)
|
http.Redirect(w, r, ar.cfg.ClientRedirectURL+"/?"+value.Encode(), http.StatusTemporaryRedirect)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
r.Get("/me", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// get access token from cookie
|
||||||
|
cookie, err := r.Cookie("access_token")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Access token not found", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get userInfo info
|
||||||
|
userInfo, err := ar.oauth.GetUserInfo(r.Context(), &oauth2.Token{AccessToken: cookie.Value})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error getting userInfo info", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// map with database
|
||||||
|
userFromDb, err := ar.userService.GetUserByEmail(r.Context(), userInfo.Email)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error while getting user data from database.", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if userFromDb == nil {
|
||||||
|
http.Error(w, "Unauthorized, We not found your email, Please contact admin.", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
picture := userInfo.Picture
|
||||||
|
if userFromDb.Picture != "" {
|
||||||
|
picture = userFromDb.Picture
|
||||||
|
}
|
||||||
|
|
||||||
|
value := url.Values{
|
||||||
|
"id": {userFromDb.ID},
|
||||||
|
"name": {userFromDb.Name},
|
||||||
|
"email": {userInfo.Email},
|
||||||
|
"picture": {picture},
|
||||||
|
"permissions": {strconv.Itoa(int(userFromDb.Permissions))},
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(value)
|
||||||
|
})
|
||||||
|
|
||||||
r.Get("/refresh", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/refresh", func(w http.ResponseWriter, r *http.Request) {
|
||||||
// get refresh token from query string
|
// get refresh token from query string
|
||||||
refreshToken := r.URL.Query().Get("refresh_token")
|
refreshToken := r.URL.Query().Get("refresh_token")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue