update filemanager navigation
This commit is contained in:
parent
11dc6b2132
commit
92b11f7b9d
31 changed files with 363 additions and 305 deletions
|
|
@ -11,7 +11,7 @@ module.exports = {
|
||||||
'plugin:react-hooks/recommended',
|
'plugin:react-hooks/recommended',
|
||||||
'prettier'
|
'prettier'
|
||||||
],
|
],
|
||||||
ignorePatterns: ['types/env.d.ts', 'node_modules/**', '**/dist/**', 'scr/components/ui/**'],
|
ignorePatterns: ['types/env.d.ts', 'node_modules/**', '**/dist/**', 'src/components/ui/**'],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 12,
|
ecmaVersion: 12,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export function AdbDaemon(_win: BrowserWindow | null, ipcMain: Electron.IpcMain)
|
||||||
await createConnection()
|
await createConnection()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('adb:shell', async (event, command: string) => {
|
ipcMain.handle('adb:shell', async (_event, command: string) => {
|
||||||
if (!adb) {
|
if (!adb) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,22 +12,19 @@ export default function (
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('open-url', (_event, url) => {
|
app.on('open-url', (_event, url) => {
|
||||||
const paramsString = url.split('://')[1]
|
url = url.replace('taobin-electron://', 'http://')
|
||||||
|
const parsedUrl = new URL(url)
|
||||||
|
|
||||||
const kind = paramsString.split('?')[0]
|
if (parsedUrl.host === 'login') {
|
||||||
|
|
||||||
const params = new URLSearchParams(paramsString)
|
|
||||||
|
|
||||||
if (kind === '/login') {
|
|
||||||
win?.webContents.send('loginSuccess', {
|
win?.webContents.send('loginSuccess', {
|
||||||
id: params.get('id'),
|
id: parsedUrl.searchParams.get('id'),
|
||||||
name: params.get('name'),
|
email: parsedUrl.searchParams.get('email'),
|
||||||
email: params.get('email'),
|
name: parsedUrl.searchParams.get('name'),
|
||||||
picture: params.get('picture'),
|
picture: parsedUrl.searchParams.get('picture'),
|
||||||
permissions: params.get('permissions'),
|
permissions: parsedUrl.searchParams.get('permissions'),
|
||||||
access_token: params.get('access_token'),
|
accessToken: parsedUrl.searchParams.get('access_token'),
|
||||||
max_age: params.get('access_token_max_age'),
|
maxAge: parsedUrl.searchParams.get('max_age'),
|
||||||
refresh_token: params.get('refresh_token')
|
refreshToken: parsedUrl.searchParams.get('refresh_token')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { app, BrowserWindow, ipcMain, shell } from 'electron'
|
import type { WebRequestFilter } from 'electron'
|
||||||
|
import { app, BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import deeplink from './deeplink'
|
import deeplink from './deeplink'
|
||||||
import { eventGetKeyChain } from './keychain'
|
import { eventGetKeyChain } from './keychain'
|
||||||
|
|
@ -128,4 +129,20 @@ app.whenReady().then(() => {
|
||||||
|
|
||||||
//keychain
|
//keychain
|
||||||
eventGetKeyChain(ipcMain)
|
eventGetKeyChain(ipcMain)
|
||||||
|
|
||||||
|
const filter: WebRequestFilter = {
|
||||||
|
urls: ['http://localhost:8080/*']
|
||||||
|
}
|
||||||
|
|
||||||
|
session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
|
||||||
|
details.requestHeaders['Origin'] = 'http://localhost:8080'
|
||||||
|
callback({ requestHeaders: details.requestHeaders })
|
||||||
|
})
|
||||||
|
|
||||||
|
session.defaultSession.webRequest.onHeadersReceived(filter, (details, callback) => {
|
||||||
|
if (details.responseHeaders) {
|
||||||
|
details.responseHeaders['Access-Control-Allow-Origin'] = ['*']
|
||||||
|
}
|
||||||
|
callback({ responseHeaders: details.responseHeaders })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="https://accounts.google.com/gsi/client" async></script>
|
<script src="https://accounts.google.com/gsi/client" async></script>
|
||||||
<title>Taobin Recipe Manager</title>
|
<title>Taobin Recipe Manager</title>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Document</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a href="taobin-electron://test">test</a>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -6,7 +6,7 @@ const AuthCallBack: React.FC = () => {
|
||||||
// emit message to main process
|
// emit message to main process
|
||||||
|
|
||||||
if (params.get('kind') === 'electron') {
|
if (params.get('kind') === 'electron') {
|
||||||
window.location.href = import.meta.env.TAOBIN_RECIPE_MANAGER_DEEPLINK_PROTOCOL + '/login' + window.location.search
|
window.location.href = import.meta.env.TAOBIN_RECIPE_MANAGER_DEEPLINK_PROTOCOL + 'login' + window.location.search
|
||||||
} else {
|
} else {
|
||||||
window.opener.postMessage(
|
window.opener.postMessage(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
|
@ -12,9 +12,9 @@ import {
|
||||||
DropdownMenuSubContent
|
DropdownMenuSubContent
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { DropdownMenuShortcut } from './ui/dropdown-menu'
|
|
||||||
import userAuthStore from '@/hooks/userAuth'
|
import userAuthStore from '@/hooks/userAuth'
|
||||||
import { Button } from './ui/button'
|
import { Button } from './ui/button'
|
||||||
|
import Logo from '@/assets/vite.svg'
|
||||||
|
|
||||||
const DropdownMenuUser: React.FC = () => {
|
const DropdownMenuUser: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -32,22 +32,10 @@ const DropdownMenuUser: React.FC = () => {
|
||||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||||
Profile
|
<DropdownMenuItem>Billing</DropdownMenuItem>
|
||||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem>Keyboard shortcuts</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
|
||||||
Billing
|
|
||||||
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
Settings
|
|
||||||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
Keyboard shortcuts
|
|
||||||
<DropdownMenuShortcut>⌘K</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
|
|
@ -63,20 +51,14 @@ const DropdownMenuUser: React.FC = () => {
|
||||||
</DropdownMenuSubContent>
|
</DropdownMenuSubContent>
|
||||||
</DropdownMenuPortal>
|
</DropdownMenuPortal>
|
||||||
</DropdownMenuSub>
|
</DropdownMenuSub>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>New Team</DropdownMenuItem>
|
||||||
New Team
|
|
||||||
<DropdownMenuShortcut>⌘+T</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem>GitHub</DropdownMenuItem>
|
<DropdownMenuItem>GitHub</DropdownMenuItem>
|
||||||
<DropdownMenuItem>Support</DropdownMenuItem>
|
<DropdownMenuItem>Support</DropdownMenuItem>
|
||||||
<DropdownMenuItem disabled>API</DropdownMenuItem>
|
<DropdownMenuItem disabled>API</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>Log out</DropdownMenuItem>
|
||||||
Log out
|
|
||||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)
|
)
|
||||||
|
|
@ -89,7 +71,7 @@ const Navbar: React.FC = () => {
|
||||||
<nav className="flex justify-center items-center fixed top-0 z-50 w-full py-5 bg-zinc-700">
|
<nav className="flex justify-center items-center fixed top-0 z-50 w-full py-5 bg-zinc-700">
|
||||||
<div className="flex justify-between items-center mx-auto w-full max-w-screen-2xl px-6 xs:px-8 sm:px-16">
|
<div className="flex justify-between items-center mx-auto w-full max-w-screen-2xl px-6 xs:px-8 sm:px-16">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<img src="/vite.svg" alt="logo" width={40} height={40} />
|
<img src={Logo} alt="logo" width={40} height={40} />
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="flex items-center ms-3">
|
<div className="flex items-center ms-3">
|
||||||
|
|
@ -97,9 +79,7 @@ const Navbar: React.FC = () => {
|
||||||
<DropdownMenuUser />
|
<DropdownMenuUser />
|
||||||
) : (
|
) : (
|
||||||
<Button asChild variant={'outline'}>
|
<Button asChild variant={'outline'}>
|
||||||
<Link to={'/login?redirect_to=' + window.location.pathname}>
|
<Link to={'/login?redirect_to=' + window.location.pathname}>Login</Link>
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,70 +1,60 @@
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
|
import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons'
|
||||||
import { DayPicker } from "react-day-picker"
|
import { DayPicker } from 'react-day-picker'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
|
|
||||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
||||||
|
|
||||||
function Calendar({
|
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
|
||||||
className,
|
|
||||||
classNames,
|
|
||||||
showOutsideDays = true,
|
|
||||||
...props
|
|
||||||
}: CalendarProps) {
|
|
||||||
return (
|
return (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
showOutsideDays={showOutsideDays}
|
showOutsideDays={showOutsideDays}
|
||||||
className={cn("p-3", className)}
|
className={cn('p-3', className)}
|
||||||
classNames={{
|
classNames={{
|
||||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
|
||||||
month: "space-y-4",
|
month: 'space-y-4',
|
||||||
caption: "flex justify-center pt-1 relative items-center",
|
caption: 'flex justify-center pt-1 relative items-center',
|
||||||
caption_label: "text-sm font-medium",
|
caption_label: 'text-sm font-medium',
|
||||||
nav: "space-x-1 flex items-center",
|
nav: 'space-x-1 flex items-center',
|
||||||
nav_button: cn(
|
nav_button: cn(
|
||||||
buttonVariants({ variant: "outline" }),
|
buttonVariants({ variant: 'outline' }),
|
||||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
|
||||||
),
|
),
|
||||||
nav_button_previous: "absolute left-1",
|
nav_button_previous: 'absolute left-1',
|
||||||
nav_button_next: "absolute right-1",
|
nav_button_next: 'absolute right-1',
|
||||||
table: "w-full border-collapse space-y-1",
|
table: 'w-full border-collapse space-y-1',
|
||||||
head_row: "flex",
|
head_row: 'flex',
|
||||||
head_cell:
|
head_cell: 'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
|
||||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
row: 'flex w-full mt-2',
|
||||||
row: "flex w-full mt-2",
|
|
||||||
cell: cn(
|
cell: cn(
|
||||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md',
|
||||||
props.mode === "range"
|
props.mode === 'range'
|
||||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
|
||||||
: "[&:has([aria-selected])]:rounded-md"
|
: '[&:has([aria-selected])]:rounded-md'
|
||||||
),
|
),
|
||||||
day: cn(
|
day: cn(buttonVariants({ variant: 'ghost' }), 'h-8 w-8 p-0 font-normal aria-selected:opacity-100'),
|
||||||
buttonVariants({ variant: "ghost" }),
|
day_range_start: 'day-range-start',
|
||||||
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
|
day_range_end: 'day-range-end',
|
||||||
),
|
|
||||||
day_range_start: "day-range-start",
|
|
||||||
day_range_end: "day-range-end",
|
|
||||||
day_selected:
|
day_selected:
|
||||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
|
||||||
day_today: "bg-accent text-accent-foreground",
|
day_today: 'bg-accent text-accent-foreground',
|
||||||
day_outside:
|
day_outside:
|
||||||
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
|
||||||
day_disabled: "text-muted-foreground opacity-50",
|
day_disabled: 'text-muted-foreground opacity-50',
|
||||||
day_range_middle:
|
day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
|
||||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
day_hidden: 'invisible',
|
||||||
day_hidden: "invisible",
|
...classNames
|
||||||
...classNames,
|
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
|
IconLeft: () => <ChevronLeftIcon className="h-4 w-4" />,
|
||||||
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
|
IconRight: () => <ChevronRightIcon className="h-4 w-4" />
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Calendar.displayName = "Calendar"
|
Calendar.displayName = 'Calendar'
|
||||||
|
|
||||||
export { Calendar }
|
export { Calendar }
|
||||||
|
|
|
||||||
65
client-electron/src/hooks/axios.ts
Normal file
65
client-electron/src/hooks/axios.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import axios, { type AxiosInstance } from 'axios'
|
||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
interface AxiosHook {
|
||||||
|
axios: AxiosInstance
|
||||||
|
accessToken: string
|
||||||
|
setAccessToken: (accessToken: string) => void
|
||||||
|
refreshToken: string
|
||||||
|
setRefreshToken: (refreshToken: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useAxios = create<AxiosHook>((set, get) => ({
|
||||||
|
accessToken: '',
|
||||||
|
setAccessToken: (accessToken: string) => {
|
||||||
|
get().axios.defaults.headers.common['X-Access-Token'] = accessToken
|
||||||
|
|
||||||
|
window.ipcRenderer.invoke('set-keyChain', {
|
||||||
|
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||||
|
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
|
||||||
|
password: accessToken
|
||||||
|
})
|
||||||
|
|
||||||
|
set({ accessToken })
|
||||||
|
},
|
||||||
|
refreshToken: '',
|
||||||
|
setRefreshToken: (refreshToken: string) => {
|
||||||
|
get().axios.defaults.headers.common['X-Refresh-Token'] = refreshToken
|
||||||
|
|
||||||
|
window.ipcRenderer.invoke('set-keyChain', {
|
||||||
|
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||||
|
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
|
||||||
|
password: refreshToken
|
||||||
|
})
|
||||||
|
|
||||||
|
set({ refreshToken })
|
||||||
|
},
|
||||||
|
axios: axios.create({
|
||||||
|
baseURL: import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL ?? 'http://localhost:8080',
|
||||||
|
timeout: 3000
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
useAxios.getState().axios.interceptors.response.use(
|
||||||
|
res => res,
|
||||||
|
async err => {
|
||||||
|
const originalRequest = err.config
|
||||||
|
|
||||||
|
if (err.response.status === 401 && !originalRequest._retry) {
|
||||||
|
originalRequest._retry = true
|
||||||
|
|
||||||
|
return useAxios
|
||||||
|
.getState()
|
||||||
|
.axios.post('/auth/refresh', null, { withCredentials: true })
|
||||||
|
.then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
return useAxios.getState().axios(originalRequest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default useAxios
|
||||||
|
|
@ -1,17 +1,10 @@
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import useAdb from './useAdb'
|
import useAdb from './useAdb'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
import { toast } from '@/components/ui/use-toast'
|
||||||
import { type LinuxFileType } from '@yume-chan/adb'
|
|
||||||
import { fromUnixTime } from 'date-fns'
|
import { fromUnixTime } from 'date-fns'
|
||||||
import { Consumable, ReadableStream, WritableStream } from '@yume-chan/stream-extra'
|
import { Consumable, ReadableStream, WritableStream } from '@yume-chan/stream-extra'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
|
import { type AndroidFile } from '@/models/android/schema'
|
||||||
export interface AndroidFile {
|
|
||||||
filename: string
|
|
||||||
type: LinuxFileType
|
|
||||||
size: bigint
|
|
||||||
dateModified: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FileManagerAndroidHook {
|
interface FileManagerAndroidHook {
|
||||||
rootPath: string
|
rootPath: string
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||||
import { RecipeDashboardFilterQuery } from './recipe-dashboard'
|
import { type RecipeDashboardFilterQuery } from './recipe-dashboard'
|
||||||
|
|
||||||
interface localStorageHook {
|
interface localStorageHook {
|
||||||
recipeQuery?: RecipeDashboardFilterQuery
|
recipeQuery?: RecipeDashboardFilterQuery
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import customAxios from '@/lib/customAxios'
|
|
||||||
import { type RecipeDashboard } from '@/models/recipe/schema'
|
import { type RecipeDashboard } from '@/models/recipe/schema'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
|
import useAxios from './axios'
|
||||||
|
|
||||||
export interface RecipeDashboardFilterQuery {
|
export interface RecipeDashboardFilterQuery {
|
||||||
countryID: string
|
countryID: string
|
||||||
|
|
@ -19,8 +19,9 @@ interface RecipeDashboardHook {
|
||||||
|
|
||||||
const useRecipeDashboard = create<RecipeDashboardHook>(() => ({
|
const useRecipeDashboard = create<RecipeDashboardHook>(() => ({
|
||||||
async getRecipesDashboard(filter) {
|
async getRecipesDashboard(filter) {
|
||||||
return customAxios
|
return useAxios
|
||||||
.get<RecipeDashboard[]>('/v2/recipes/dashboard', {
|
.getState()
|
||||||
|
.axios.get<RecipeDashboard[]>('/v2/recipes/dashboard', {
|
||||||
params: filter
|
params: filter
|
||||||
? {
|
? {
|
||||||
country_id: filter.countryID,
|
country_id: filter.countryID,
|
||||||
|
|
@ -34,8 +35,9 @@ const useRecipeDashboard = create<RecipeDashboardHook>(() => ({
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
},
|
},
|
||||||
async getMaterials(filter) {
|
async getMaterials(filter) {
|
||||||
return customAxios
|
return useAxios
|
||||||
.get<materialDashboard[]>('/v2/materials/dashboard', {
|
.getState()
|
||||||
|
.axios.get<materialDashboard[]>('/v2/materials/dashboard', {
|
||||||
params: filter
|
params: filter
|
||||||
? {
|
? {
|
||||||
country_id: filter.countryID,
|
country_id: filter.countryID,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import customAxios from '../lib/customAxios'
|
|
||||||
import type { User } from '@/models/user/schema'
|
import type { User } from '@/models/user/schema'
|
||||||
|
import useAxios from './axios'
|
||||||
|
|
||||||
interface UserAuth {
|
interface UserAuth {
|
||||||
userInfo: User | null
|
userInfo: User | null
|
||||||
|
|
@ -12,20 +12,18 @@ interface UserAuth {
|
||||||
const userAuthStore = create<UserAuth>(set => ({
|
const userAuthStore = create<UserAuth>(set => ({
|
||||||
userInfo: null,
|
userInfo: null,
|
||||||
setUserInfo: userInfo => set({ userInfo }),
|
setUserInfo: userInfo => set({ userInfo }),
|
||||||
getUserInfo: () => {
|
getUserInfo: async () => {
|
||||||
return customAxios
|
try {
|
||||||
.get<User>('/user/me')
|
const res = await useAxios.getState().axios.get<User>('/auth/me')
|
||||||
.then(res => {
|
set({ userInfo: res.data })
|
||||||
set({ userInfo: res.data })
|
return res.data
|
||||||
return res.data
|
} catch {
|
||||||
})
|
set({ userInfo: null })
|
||||||
.catch(() => {
|
return null
|
||||||
set({ userInfo: null })
|
}
|
||||||
return null
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
logout: () => {
|
logout: () => {
|
||||||
customAxios.post('/auth/logout')
|
useAxios.getState().axios.post('/auth/logout')
|
||||||
set({ userInfo: null })
|
set({ userInfo: null })
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { Outlet, useNavigate } from 'react-router-dom'
|
import { Outlet, useNavigate } from 'react-router-dom'
|
||||||
import Navbar from '../components/header'
|
import Navbar from '../components/navbar'
|
||||||
import Sidebar, { type MenuList } from '../components/sidebar'
|
import Sidebar, { type MenuList } from '../components/sidebar'
|
||||||
import userAuthStore from '../hooks/userAuth'
|
import userAuthStore from '../hooks/userAuth'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import { useCallback } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { Toaster } from '@/components/ui/toaster'
|
import { Toaster } from '@/components/ui/toaster'
|
||||||
|
import useAxios from '@/hooks/axios'
|
||||||
|
|
||||||
interface MainLayoutProps {
|
interface MainLayoutProps {
|
||||||
sidebarMenu: MenuList
|
sidebarMenu: MenuList
|
||||||
|
|
@ -21,17 +22,38 @@ const MainLayout: React.FC<MainLayoutProps> = ({ sidebarMenu }) => {
|
||||||
// get current path
|
// get current path
|
||||||
const currentPath = window.location.pathname
|
const currentPath = window.location.pathname
|
||||||
|
|
||||||
useCallback(() => {
|
useEffect(() => {
|
||||||
console.log(import.meta.env.NODE_ENV)
|
console.log('ENV:', import.meta.env.MODE)
|
||||||
if (!userInfo && import.meta.env.NODE_ENV !== 'development') {
|
|
||||||
getUserInfo().then(userInfo => {
|
// Sync keyChain
|
||||||
// if still not login then redirect to login page
|
const syncKeyChain = async () => {
|
||||||
if (!userInfo) {
|
const tokens: {
|
||||||
navigate('/login?redirect=' + currentPath)
|
account: string
|
||||||
}
|
password: string
|
||||||
})
|
}[] = await window.ipcRenderer.invoke(
|
||||||
|
'keyChainSync',
|
||||||
|
import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME
|
||||||
|
)
|
||||||
|
if (tokens.length > 0) {
|
||||||
|
const [accessToken, refreshToken] = tokens
|
||||||
|
console.log(accessToken)
|
||||||
|
console.log(refreshToken)
|
||||||
|
useAxios.getState().setAccessToken(accessToken.password)
|
||||||
|
useAxios.getState().setRefreshToken(refreshToken.password)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [userInfo, getUserInfo, navigate, currentPath])
|
|
||||||
|
syncKeyChain().then(() => {
|
||||||
|
if (!userInfo) {
|
||||||
|
getUserInfo().then(userInfo => {
|
||||||
|
// if still not login then redirect to login page
|
||||||
|
if (!userInfo && import.meta.env.PROD) {
|
||||||
|
navigate('/login?redirect=' + currentPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [userInfo])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
const customAxios = axios.create({
|
|
||||||
baseURL: import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL ?? 'http://localhost:8080',
|
|
||||||
withCredentials: true
|
|
||||||
})
|
|
||||||
|
|
||||||
customAxios.interceptors.response.use(
|
|
||||||
res => res,
|
|
||||||
async err => {
|
|
||||||
const originalRequest = err.config
|
|
||||||
|
|
||||||
if (err.response.status === 401 && !originalRequest._retry) {
|
|
||||||
originalRequest._retry = true
|
|
||||||
|
|
||||||
console.log('refreshing token')
|
|
||||||
|
|
||||||
const refreshToken = await window.ipcRenderer.invoke(
|
|
||||||
'get-keyChain',
|
|
||||||
import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
|
||||||
import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN
|
|
||||||
)
|
|
||||||
|
|
||||||
return customAxios
|
|
||||||
.post('/auth/refresh', {
|
|
||||||
refresh_token: refreshToken
|
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
if (res.status === 200) {
|
|
||||||
return customAxios(originalRequest)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(err)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export default customAxios
|
|
||||||
|
|
@ -4,9 +4,9 @@ import App from './App.tsx'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
// <React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
// </React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (window.electronRuntime) {
|
if (window.electronRuntime) {
|
||||||
|
|
@ -14,36 +14,10 @@ if (window.electronRuntime) {
|
||||||
postMessage({ payload: 'removeLoading' }, '*')
|
postMessage({ payload: 'removeLoading' }, '*')
|
||||||
|
|
||||||
// Use contextBridge
|
// Use contextBridge
|
||||||
window.ipcRenderer.on('main-process-message', (_event, message) => {
|
window.ipcRenderer.on('main-process-message', async (_event, message) => {
|
||||||
console.log(message)
|
console.log(message)
|
||||||
})
|
})
|
||||||
|
|
||||||
// window.ipcRenderer
|
|
||||||
// .invoke('keyChainSync', import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME)
|
|
||||||
// .then(([accessToken, refreshToken]) => {
|
|
||||||
// console.log(accessToken, refreshToken)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// window.ipcRenderer
|
|
||||||
// .invoke(
|
|
||||||
// 'get-keyChain',
|
|
||||||
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
|
||||||
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN
|
|
||||||
// )
|
|
||||||
// .then(token => {
|
|
||||||
// console.log(token)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// window.ipcRenderer
|
|
||||||
// .invoke(
|
|
||||||
// 'get-keyChain',
|
|
||||||
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
|
||||||
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN
|
|
||||||
// )
|
|
||||||
// .then(token => {
|
|
||||||
// console.log(token)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// Use deep link
|
// Use deep link
|
||||||
window.ipcRenderer.on('deeplink', (_event, url) => {
|
window.ipcRenderer.on('deeplink', (_event, url) => {
|
||||||
console.log(url)
|
console.log(url)
|
||||||
|
|
|
||||||
10
client-electron/src/models/android/schema.ts
Normal file
10
client-electron/src/models/android/schema.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const AndroidFileSchema = z.object({
|
||||||
|
filename: z.string(),
|
||||||
|
type: z.number(),
|
||||||
|
size: z.bigint(),
|
||||||
|
dateModified: z.date()
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AndroidFile = z.infer<typeof AndroidFileSchema>
|
||||||
|
|
@ -21,18 +21,18 @@ const LoginPage: React.FC = () => {
|
||||||
window.ipcRenderer.on('loginSuccess', (_event, data) => {
|
window.ipcRenderer.on('loginSuccess', (_event, data) => {
|
||||||
console.log(data)
|
console.log(data)
|
||||||
|
|
||||||
const { access_token, max_age, refresh_token } = data
|
const { accessToken, maxAge, refreshToken } = data
|
||||||
|
|
||||||
window.ipcRenderer.send('set-keyChain', {
|
window.ipcRenderer.send('set-keyChain', {
|
||||||
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||||
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
|
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
|
||||||
password: access_token + ';' + max_age
|
password: accessToken + ';' + maxAge
|
||||||
})
|
})
|
||||||
|
|
||||||
window.ipcRenderer.send('set-keyChain', {
|
window.ipcRenderer.send('set-keyChain', {
|
||||||
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||||
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
|
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
|
||||||
password: refresh_token
|
password: refreshToken
|
||||||
})
|
})
|
||||||
|
|
||||||
data.permissions = Number(data.permissions)
|
data.permissions = Number(data.permissions)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const AndroidPage: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<div className="flex w-full p-5">
|
<div className="flex w-full py-5">
|
||||||
<ToolBar manager={manager} device={device} adb={adb} setAdb={setAdb} setDevice={setDevice} />
|
<ToolBar manager={manager} device={device} adb={adb} setAdb={setAdb} setDevice={setDevice} />
|
||||||
</div>
|
</div>
|
||||||
<Tabs defaultValue="scrcpy" className="w-full">
|
<Tabs defaultValue="scrcpy" className="w-full">
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import useFileManager, { type AndroidFile } from '@/hooks/filemanager-android'
|
import useFileManager from '@/hooks/filemanager-android'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import DataTable from './filemanager-table/data-table'
|
import DataTable from './filemanager-table/data-table'
|
||||||
import { ChevronRightIcon, FileIcon } from '@radix-ui/react-icons'
|
import { FileIcon } from '@radix-ui/react-icons'
|
||||||
import { type ColumnDef } from '@tanstack/react-table'
|
import { type ColumnDef } from '@tanstack/react-table'
|
||||||
import { LinuxFileType } from '@yume-chan/adb'
|
import { LinuxFileType } from '@yume-chan/adb'
|
||||||
import { formatDate } from 'date-fns'
|
import { formatDate } from 'date-fns'
|
||||||
import DataTableColumnHeader from './filemanager-table/data-table-column-header'
|
import DataTableColumnHeader from './filemanager-table/data-table-column-header'
|
||||||
import DataTableRowActions from './filemanager-table/data-table-row-actions'
|
import DataTableRowActions from './filemanager-table/data-table-row-actions'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import { type AndroidFile } from '@/models/android/schema'
|
||||||
|
|
||||||
export const FileManagerTab: React.FC = () => {
|
export const FileManagerTab: React.FC = () => {
|
||||||
const { currentPath, pushPath, popPath, setCurrentPath, scanPath } = useFileManager(
|
const { currentPath, pushPath, scanPath } = useFileManager(
|
||||||
useShallow(state => ({
|
useShallow(state => ({
|
||||||
currentPath: state.currentPath,
|
currentPath: state.currentPath,
|
||||||
pushPath: state.pushPath,
|
pushPath: state.pushPath,
|
||||||
|
|
@ -57,7 +58,7 @@ export const FileManagerTab: React.FC = () => {
|
||||||
enableHiding: false
|
enableHiding: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'filename',
|
accessorKey: 'filename',
|
||||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -73,22 +74,52 @@ export const FileManagerTab: React.FC = () => {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'type',
|
||||||
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Type" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <div>{row.original.type === LinuxFileType.File ? 'File' : 'Directory'}</div>
|
||||||
},
|
},
|
||||||
enableSorting: true,
|
filterFn: (row, id, value) => {
|
||||||
enableHiding: false
|
return value.includes(row.getValue(id))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'size',
|
accessorKey: 'size',
|
||||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Size" />,
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Size" />,
|
||||||
cell: ({ row }) => <div>{row.original.size.toString()}</div>
|
cell: ({ row }) => {
|
||||||
|
if (row.original.type === LinuxFileType.Directory) return <div>-</div>
|
||||||
|
// show size in B, KB, MB, GB
|
||||||
|
let size = Number(row.original.size)
|
||||||
|
let unit = 'B'
|
||||||
|
if (size > 1024) {
|
||||||
|
size /= 1024
|
||||||
|
unit = 'KB'
|
||||||
|
}
|
||||||
|
if (size > 1024) {
|
||||||
|
size /= 1024
|
||||||
|
unit = 'MB'
|
||||||
|
}
|
||||||
|
if (size > 1024) {
|
||||||
|
size /= 1024
|
||||||
|
unit = 'GB'
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{size.toFixed(2)} {unit}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dateModified',
|
accessorKey: 'dateModified',
|
||||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Date" />,
|
header: ({ column }) => <DataTableColumnHeader column={column} title="Date" />,
|
||||||
cell: ({ row }) => <div>{formatDate(row.original.dateModified, 'dd MMM yyyy HH:mm:ss')}</div>
|
cell: ({ row }) => <div>{formatDate(row.original.dateModified, 'dd MMM yyyy HH:mm:ss')}</div>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
accessorKey: 'actions',
|
||||||
cell: ({ row }) => <DataTableRowActions row={row} />
|
cell: ({ row }) => <DataTableRowActions row={row} />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -100,32 +131,6 @@ export const FileManagerTab: React.FC = () => {
|
||||||
<CardDescription>Manage files in Android</CardDescription>
|
<CardDescription>Manage files in Android</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex space-x-3">
|
|
||||||
<span className="hover:underline" onClick={() => setCurrentPath('')}>
|
|
||||||
ROOT
|
|
||||||
</span>
|
|
||||||
{currentPath &&
|
|
||||||
currentPath.split('/').map((path, index) => {
|
|
||||||
return (
|
|
||||||
<div key={index} className="flex justify-center items-center space-x-3">
|
|
||||||
<span
|
|
||||||
className="hover:underline"
|
|
||||||
onClick={() =>
|
|
||||||
setCurrentPath(
|
|
||||||
currentPath
|
|
||||||
.split('/')
|
|
||||||
.slice(0, index + 1)
|
|
||||||
.join('/')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{path}
|
|
||||||
</span>
|
|
||||||
{currentPath.split('/').length - 1 !== index && <ChevronRightIcon />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<DataTable data={files ?? []} columns={columns} isLoading={isLoading} />
|
<DataTable data={files ?? []} columns={columns} isLoading={isLoading} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,14 @@ import {
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { DotsHorizontalIcon } from '@radix-ui/react-icons'
|
import { DotsHorizontalIcon } from '@radix-ui/react-icons'
|
||||||
import { labels } from '@/models/data'
|
import { labels } from '@/models/data'
|
||||||
|
import { AndroidFileSchema } from '@/models/android/schema'
|
||||||
|
|
||||||
interface DataTableRowActionsProps<TData> {
|
interface DataTableRowActionsProps<TData> {
|
||||||
row: Row<TData>
|
row: Row<TData>
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataTableRowActions = <TData,>({ row }: DataTableRowActionsProps<TData>) => {
|
const DataTableRowActions = <TData,>({ row }: DataTableRowActionsProps<TData>) => {
|
||||||
const task = {
|
const task = AndroidFileSchema.parse(row.original)
|
||||||
label: 'none'
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
@ -40,7 +39,7 @@ const DataTableRowActions = <TData,>({ row }: DataTableRowActionsProps<TData>) =
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>Labels</DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>Labels</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuSubContent>
|
<DropdownMenuSubContent>
|
||||||
<DropdownMenuRadioGroup value={task.label}>
|
<DropdownMenuRadioGroup value={task.filename}>
|
||||||
{labels.map(label => (
|
{labels.map(label => (
|
||||||
<DropdownMenuRadioItem key={label.value} value={label.value}>
|
<DropdownMenuRadioItem key={label.value} value={label.value}>
|
||||||
{label.label}
|
{label.label}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,42 @@
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { type Table } from '@tanstack/react-table'
|
import { type Table } from '@tanstack/react-table'
|
||||||
import DataTableViewOptions from './data-table-view-options'
|
import DataTableViewOptions from './data-table-view-options'
|
||||||
import { Cross2Icon } from '@radix-ui/react-icons'
|
import { ArrowUpIcon, Cross2Icon } from '@radix-ui/react-icons'
|
||||||
|
import DataTableFacetedFilter from './data-table-faceted-filter'
|
||||||
|
import useFileManager from '@/hooks/filemanager-android'
|
||||||
|
|
||||||
interface DataTableToolbarProps<TData> {
|
interface DataTableToolbarProps<TData> {
|
||||||
table: Table<TData>
|
table: Table<TData>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const type = [
|
||||||
|
{ value: 'file', label: 'File' },
|
||||||
|
{ value: 'directory', label: 'Directory' }
|
||||||
|
]
|
||||||
|
|
||||||
const DataTableToolbar = <TData,>({ table }: DataTableToolbarProps<TData>) => {
|
const DataTableToolbar = <TData,>({ table }: DataTableToolbarProps<TData>) => {
|
||||||
const isFiltered = table.getState().columnFilters.length > 0
|
const isFiltered = table.getState().columnFilters.length > 0
|
||||||
|
const popPath = useFileManager(state => state.popPath)
|
||||||
|
const currentPath = useFileManager(state => state.currentPath)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex w-full flex-col space-y-4 rounded-lg bg-white p-3 shadow-md">
|
<div className="flex w-full flex-col space-y-4 rounded-lg bg-white p-3 shadow-md">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-1 items-center space-x-2">
|
<div className="flex flex-1 items-center space-x-2">
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
disabled={currentPath ? false : true}
|
||||||
|
onClick={popPath}
|
||||||
|
className="h-8 px-2 lg:px-3"
|
||||||
|
>
|
||||||
|
<ArrowUpIcon className="h-4 w-4" /> Back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{table.getColumn('type') && (
|
||||||
|
<DataTableFacetedFilter column={table.getColumn('type')} title="Type" options={type} />
|
||||||
|
)}
|
||||||
{isFiltered && (
|
{isFiltered && (
|
||||||
<Button variant="ghost" onClick={() => table.resetColumnFilters()} className="h-8 px-2 lg:px-3">
|
<Button variant="ghost" onClick={() => table.resetColumnFilters()} className="h-8 px-2 lg:px-3">
|
||||||
Reset
|
Reset
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
useReactTable,
|
useReactTable,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
getFilteredRowModel,
|
getFilteredRowModel,
|
||||||
getPaginationRowModel,
|
|
||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
getFacetedRowModel,
|
getFacetedRowModel,
|
||||||
getFacetedUniqueValues,
|
getFacetedUniqueValues,
|
||||||
|
|
@ -12,7 +11,6 @@ import {
|
||||||
import { rankItem } from '@tanstack/match-sorter-utils'
|
import { rankItem } from '@tanstack/match-sorter-utils'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
import DataTablePagination from './data-table-pagination'
|
|
||||||
import { ReloadIcon } from '@radix-ui/react-icons'
|
import { ReloadIcon } from '@radix-ui/react-icons'
|
||||||
import DataTableToolbar from './data-table-toolbar'
|
import DataTableToolbar from './data-table-toolbar'
|
||||||
|
|
||||||
|
|
@ -40,7 +38,12 @@ const DataTable = <TData, TValue>({ columns, data, isLoading }: DataTableProps<T
|
||||||
const [rowSelection, setRowSelection] = useState({})
|
const [rowSelection, setRowSelection] = useState({})
|
||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||||
const [sorting, setSorting] = useState<SortingState>([])
|
const [sorting, setSorting] = useState<SortingState>([
|
||||||
|
{
|
||||||
|
id: 'type',
|
||||||
|
desc: false
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
|
|
@ -62,7 +65,6 @@ const DataTable = <TData, TValue>({ columns, data, isLoading }: DataTableProps<T
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getFacetedRowModel: getFacetedRowModel(),
|
getFacetedRowModel: getFacetedRowModel(),
|
||||||
getFacetedUniqueValues: getFacetedUniqueValues()
|
getFacetedUniqueValues: getFacetedUniqueValues()
|
||||||
|
|
@ -70,7 +72,6 @@ const DataTable = <TData, TValue>({ columns, data, isLoading }: DataTableProps<T
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<DataTableToolbar table={table} />
|
<DataTableToolbar table={table} />
|
||||||
|
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
|
|
@ -112,7 +113,6 @@ const DataTable = <TData, TValue>({ columns, data, isLoading }: DataTableProps<T
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
<DataTablePagination table={table} />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ export const ToolBar: React.FC<ToolBarProps> = ({ manager, adb, device, setAdb,
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center space-x-5 w-full p-4 shadow-lg rounded-lg">
|
<div className="flex justify-between items-center w-full p-4 shadow-lg rounded-lg">
|
||||||
{adb ? (
|
{adb ? (
|
||||||
<div className="flex flex-col justify-center items-start">
|
<div className="flex flex-col justify-center items-start">
|
||||||
<ul className="list-disc pl-4">
|
<ul className="list-disc pl-4">
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import DataTableColumnHeader from './data-table-column-header'
|
||||||
import DataTableRowActions from './data-table-row-actions'
|
import DataTableRowActions from './data-table-row-actions'
|
||||||
import { CheckCircledIcon, CrossCircledIcon } from '@radix-ui/react-icons'
|
import { CheckCircledIcon, CrossCircledIcon } from '@radix-ui/react-icons'
|
||||||
import * as dateFormat from 'date-fns'
|
import * as dateFormat from 'date-fns'
|
||||||
import { DateRange } from 'react-day-picker'
|
import { type DateRange } from 'react-day-picker'
|
||||||
|
|
||||||
export const columns: ColumnDef<RecipeDashboard>[] = [
|
export const columns: ColumnDef<RecipeDashboard>[] = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Calendar } from '@/components/ui/calendar'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -7,23 +8,52 @@ import {
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons'
|
import { ArrowDownIcon, ArrowUpIcon, CalendarIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons'
|
||||||
import { type Column } from '@tanstack/react-table'
|
import { type Column } from '@tanstack/react-table'
|
||||||
|
import { type DateRange } from 'react-day-picker'
|
||||||
|
|
||||||
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
|
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
column: Column<TData, TValue>
|
column: Column<TData, TValue>
|
||||||
title: string
|
title: string
|
||||||
|
isDate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataTableColumnHeader = <TData, TValue>({
|
const DataTableColumnHeader = <TData, TValue>({
|
||||||
column,
|
column,
|
||||||
title,
|
title,
|
||||||
|
isDate,
|
||||||
className
|
className
|
||||||
}: DataTableColumnHeaderProps<TData, TValue>) => {
|
}: DataTableColumnHeaderProps<TData, TValue>) => {
|
||||||
if (!column.getCanSort()) {
|
if (!column.getCanSort() && !isDate) {
|
||||||
return <div className={cn(className)}>{title}</div>
|
return <div className={cn(className)}>{title}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDate) {
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center space-x-2', className)}>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="data-[state=open]:bg-accent -ml-3 h-8">
|
||||||
|
<span>{title}</span>
|
||||||
|
<CalendarIcon className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="center">
|
||||||
|
<Calendar
|
||||||
|
mode="range"
|
||||||
|
defaultMonth={new Date('2022-01-01')}
|
||||||
|
selected={column.getFilterValue() as DateRange}
|
||||||
|
onSelect={date => column.setFilterValue(date)}
|
||||||
|
numberOfMonths={2}
|
||||||
|
disabled={date => date > new Date() || date < new Date('1900-01-01')}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center space-x-2', className)}>
|
<div className={cn('flex items-center space-x-2', className)}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
ServerPort int `mapstructure:"SERVER_PORT"`
|
ServerPort int `mapstructure:"SERVER_PORT"`
|
||||||
TusServerPort int `mapstructure:"TUS_SERVER_PORT"`
|
TusServerPort int `mapstructure:"TUS_SERVER_PORT"`
|
||||||
AllowedOrigins string `mapstructure:"ALLOWED_ORIGINS"`
|
AllowedOrigins string `mapstructure:"ALLOWED_ORIGINS"`
|
||||||
ClientRedirectURL string `mapstructure:"CLIENT_REDIRECT_URL"`
|
ClientRedirectURL string `mapstructure:"CLIENT_REDIRECT_URL"`
|
||||||
ClientElectronRedirectURL string `mapstructure:"CLIENT_ELECTRON_REDIRECT_URL"`
|
ServerDomain string `mapstructure:"SERVER_DOMAIN"`
|
||||||
ServerDomain string `mapstructure:"SERVER_DOMAIN"`
|
APIKey string `mapstructure:"API_KEY"`
|
||||||
APIKey string `mapstructure:"API_KEY"`
|
Debug bool `mapstructure:"DEBUG_MODE"`
|
||||||
Debug bool `mapstructure:"DEBUG_MODE"`
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"recipe-manager/enums/permissions"
|
"recipe-manager/enums/permissions"
|
||||||
"recipe-manager/models"
|
"recipe-manager/models"
|
||||||
"recipe-manager/services/oauth"
|
"recipe-manager/services/oauth"
|
||||||
"recipe-manager/services/user"
|
"recipe-manager/services/user"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ========================== ValidatePermissions =========================================
|
// ========================== ValidatePermissions =========================================
|
||||||
|
|
@ -20,6 +21,8 @@ func Authorize(oauthService oauth.OAuthService, userService user.UserService, ne
|
||||||
|
|
||||||
if cookie, err := r.Cookie("access_token"); err == nil {
|
if cookie, err := r.Cookie("access_token"); err == nil {
|
||||||
token.AccessToken = cookie.Value
|
token.AccessToken = cookie.Value
|
||||||
|
} else {
|
||||||
|
token.AccessToken = r.Header.Get("X-Access-Token")
|
||||||
}
|
}
|
||||||
|
|
||||||
userInfo, err := oauthService.GetUserInfo(r.Context(), token)
|
userInfo, err := oauthService.GetUserInfo(r.Context(), token)
|
||||||
|
|
@ -28,6 +31,8 @@ func Authorize(oauthService oauth.OAuthService, userService user.UserService, ne
|
||||||
// if have refresh token, set refresh token to token
|
// if have refresh token, set refresh token to token
|
||||||
if cookie, err := r.Cookie("refresh_token"); err == nil {
|
if cookie, err := r.Cookie("refresh_token"); err == nil {
|
||||||
token.RefreshToken = cookie.Value
|
token.RefreshToken = cookie.Value
|
||||||
|
} else {
|
||||||
|
token.RefreshToken = r.Header.Get("X-Refresh-Token")
|
||||||
}
|
}
|
||||||
|
|
||||||
newToken, err := oauthService.RefreshToken(r.Context(), token)
|
newToken, err := oauthService.RefreshToken(r.Context(), token)
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ func (ar *AuthRouter) Route(r chi.Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
authURL := ar.oauth.AuthURL(state, stateMap)
|
authURL := ar.oauth.AuthURL(state, stateMap)
|
||||||
|
ar.taoLogger.Log.Info("User Log-In", zap.String("authURL", authURL))
|
||||||
http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
|
http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -129,12 +130,12 @@ 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 is electron then add kind to value
|
||||||
if kind == "electron" {
|
if kind == "electron" {
|
||||||
|
value.Add("kind", kind)
|
||||||
value.Add("access_token", token.AccessToken)
|
value.Add("access_token", token.AccessToken)
|
||||||
value.Add("max_age", "3600")
|
value.Add("max_age", "3600")
|
||||||
value.Add("refresh_token", token.RefreshToken)
|
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")
|
||||||
|
|
@ -143,15 +144,22 @@ func (ar *AuthRouter) Route(r chi.Router) {
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Get("/me", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/me", func(w http.ResponseWriter, r *http.Request) {
|
||||||
// get access token from cookie
|
// get access token from token
|
||||||
|
var token string
|
||||||
cookie, err := r.Cookie("access_token")
|
cookie, err := r.Cookie("access_token")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Access token not found", http.StatusBadRequest)
|
token = r.Header.Get("X-Access-Token")
|
||||||
return
|
|
||||||
|
if token == "" {
|
||||||
|
http.Error(w, "Access token not found", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
token = cookie.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
// get userInfo info
|
// get userInfo info
|
||||||
userInfo, err := ar.oauth.GetUserInfo(r.Context(), &oauth2.Token{AccessToken: cookie.Value})
|
userInfo, err := ar.oauth.GetUserInfo(r.Context(), &oauth2.Token{AccessToken: token})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error getting userInfo info", http.StatusBadRequest)
|
http.Error(w, "Error getting userInfo info", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|
@ -186,26 +194,10 @@ func (ar *AuthRouter) Route(r chi.Router) {
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Post("/refresh", func(w http.ResponseWriter, r *http.Request) {
|
r.Post("/refresh", func(w http.ResponseWriter, r *http.Request) {
|
||||||
// get refresh token from query string
|
refreshToken := r.Header.Get("X-Refresh-Token")
|
||||||
|
|
||||||
// get refresh token from body and redirect_to from body and mashal it to struct
|
|
||||||
var refreshToken string
|
|
||||||
var redirectTo string
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&struct {
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
RedirectTo string `json:"redirect_to"`
|
|
||||||
}{refreshToken, redirectTo})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Error decoding body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// refreshToken := r.URL.Query().Get("refresh_token")
|
|
||||||
// redirectTo := r.URL.Query().Get("redirect_to")
|
|
||||||
if refreshToken == "" {
|
if refreshToken == "" {
|
||||||
http.Error(w, "Refresh token not found", http.StatusBadRequest)
|
http.Error(w, "Refresh token not found", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,8 +208,16 @@ func (ar *AuthRouter) Route(r chi.Router) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect to frontend with token and refresh token
|
// get userInfo info
|
||||||
http.Redirect(w, r, ar.cfg.ClientRedirectURL+"/?token="+token.AccessToken+"&redirect_to="+redirectTo, http.StatusTemporaryRedirect)
|
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")
|
||||||
|
if err := json.NewEncoder(w).Encode(&struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
MaxAge int `json:"max_age"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}{AccessToken: token.AccessToken, MaxAge: 3600, RefreshToken: token.RefreshToken}); err != nil {
|
||||||
|
http.Error(w, "Error encoding response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Get("/revoke", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/revoke", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue