update filemanager navigation

This commit is contained in:
Kenta420 2024-02-20 15:01:43 +07:00
parent 11dc6b2132
commit 92b11f7b9d
31 changed files with 363 additions and 305 deletions

View file

@ -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,

View file

@ -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
} }

View file

@ -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')
}) })
} }
}) })

View file

@ -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 })
})
}) })

View file

@ -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>

View file

@ -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>

View file

@ -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(
{ {

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

@ -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>

View file

@ -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 }

View 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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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 })
} }
})) }))

View file

@ -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 (
<> <>

View file

@ -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

View file

@ -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)

View 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>

View file

@ -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)

View file

@ -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">

View file

@ -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>

View file

@ -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}

View file

@ -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

View file

@ -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>
) )
} }

View file

@ -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">

View file

@ -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>[] = [
{ {

View file

@ -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>

View file

@ -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"`
} }

View file

@ -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)

View file

@ -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) {