diff --git a/pkg/views/.eslintrc.cjs b/pkg/views/.eslintrc.cjs index d6c9537..f41350a 100644 --- a/pkg/views/.eslintrc.cjs +++ b/pkg/views/.eslintrc.cjs @@ -1,18 +1,15 @@ +/* eslint-env node */ +require("@rushstack/eslint-patch/modern-module-resolution") + module.exports = { root: true, - env: { browser: true, es2020: true }, extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', + "plugin:vue/vue3-essential", + "eslint:recommended", + "@vue/eslint-config-typescript", + "@vue/eslint-config-prettier/skip-formatting", ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], - rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], + parserOptions: { + ecmaVersion: "latest", }, } diff --git a/pkg/views/.gitignore b/pkg/views/.gitignore index a547bf3..8ee54e8 100644 --- a/pkg/views/.gitignore +++ b/pkg/views/.gitignore @@ -8,17 +8,23 @@ pnpm-debug.log* lerna-debug.log* node_modules +.DS_Store dist dist-ssr +coverage *.local +/cypress/videos/ +/cypress/screenshots/ + # Editor directories and files .vscode/* !.vscode/extensions.json .idea -.DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? + +*.tsbuildinfo diff --git a/pkg/views/.prettierrc.json b/pkg/views/.prettierrc.json new file mode 100644 index 0000000..6404b10 --- /dev/null +++ b/pkg/views/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "tabWidth": 2, + "singleQuote": false, + "printWidth": 120, + "trailingComma": "all" +} diff --git a/pkg/views/.vscode/extensions.json b/pkg/views/.vscode/extensions.json new file mode 100644 index 0000000..0449b97 --- /dev/null +++ b/pkg/views/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] +} diff --git a/pkg/views/README.md b/pkg/views/README.md index 0d6babe..bdb10ae 100644 --- a/pkg/views/README.md +++ b/pkg/views/README.md @@ -1,30 +1,39 @@ -# React + TypeScript + Vite +# views -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +This template should help get you started developing with Vue 3 in Vite. -Currently, two official plugins are available: +## Recommended IDE Setup -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). -## Expanding the ESLint configuration +## Type Support for `.vue` Imports in TS -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. -- Configure the top-level `parserOptions` property like this: +## Customize configuration -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} +See [Vite Configuration Reference](https://vitejs.dev/config/). + +## Project Setup + +```sh +npm install ``` -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +npm run build +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +npm run lint +``` diff --git a/pkg/views/bun.lockb b/pkg/views/bun.lockb index b20726d..5f8e772 100755 Binary files a/pkg/views/bun.lockb and b/pkg/views/bun.lockb differ diff --git a/pkg/views/src/vite-env.d.ts b/pkg/views/env.d.ts similarity index 100% rename from pkg/views/src/vite-env.d.ts rename to pkg/views/env.d.ts diff --git a/pkg/views/index.html b/pkg/views/index.html index 346559b..0e55e96 100644 --- a/pkg/views/index.html +++ b/pkg/views/index.html @@ -2,12 +2,12 @@ - + - Goatpass + Solarpass -
- +
+ diff --git a/pkg/views/package.json b/pkg/views/package.json index 61e5ddc..c78c836 100644 --- a/pkg/views/package.json +++ b/pkg/views/package.json @@ -1,47 +1,43 @@ { - "name": "identity-web", - "private": true, + "name": "views", "version": "0.0.0", + "private": true, "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build --force", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "format": "prettier --write src/" }, "dependencies": { - "@emotion/react": "^11.11.3", - "@emotion/styled": "^11.11.0", - "@fontsource/roboto": "^5.0.8", - "@mui/icons-material": "^5.15.10", - "@mui/lab": "^5.0.0-alpha.166", - "@mui/material": "^5.15.10", - "@mui/x-data-grid": "^6.19.5", - "@mui/x-date-pickers": "^6.19.5", + "@fontsource/roboto": "^5.0.12", + "@mdi/font": "^7.4.47", "@unocss/reset": "^0.58.5", - "dayjs": "^1.11.10", - "localforage": "^1.10.0", - "match-sorter": "^6.3.4", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.22.1", - "react-transition-group": "^4.4.5", - "sort-by": "^1.2.0", + "pinia": "^2.1.7", "universal-cookie": "^7.1.0", - "use-debounce": "^10.0.0" + "unocss": "^0.58.5", + "vue": "^3.4.21", + "vue-router": "^4.3.0", + "vuetify": "^3.5.8" }, "devDependencies": { - "@types/node": "^20.11.20", - "@types/react": "^18.2.56", - "@types/react-dom": "^18.2.19", - "@typescript-eslint/eslint-plugin": "^7.0.2", - "@typescript-eslint/parser": "^7.0.2", - "@vitejs/plugin-react-swc": "^3.5.0", - "eslint": "^8.56.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", - "typescript": "^5.2.2", - "unocss": "^0.58.5", - "vite": "^5.1.4" + "@rushstack/eslint-patch": "^1.3.3", + "@tsconfig/node20": "^20.1.2", + "@types/node": "^20.11.25", + "@vitejs/plugin-vue": "^5.0.4", + "@vitejs/plugin-vue-jsx": "^3.1.0", + "@vue/eslint-config-prettier": "^8.0.0", + "@vue/eslint-config-typescript": "^12.0.0", + "@vue/tsconfig": "^0.5.1", + "eslint": "^8.49.0", + "eslint-plugin-vue": "^9.17.0", + "npm-run-all2": "^6.1.2", + "prettier": "^3.0.3", + "typescript": "~5.4.0", + "vite": "^5.1.5", + "vue-tsc": "^2.0.6" } } diff --git a/pkg/views/public/favicon.svg b/pkg/views/public/favicon.svg deleted file mode 100755 index 3edea9a..0000000 --- a/pkg/views/public/favicon.svg +++ /dev/null @@ -1,21 +0,0 @@ - - Logo - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pkg/views/src/assets/utils.css b/pkg/views/src/assets/utils.css new file mode 100644 index 0000000..840e4ee --- /dev/null +++ b/pkg/views/src/assets/utils.css @@ -0,0 +1,15 @@ +html, +body, +#app, +.v-application { + overflow: auto !important; + font-family: "Roboto Sans", ui-sans-serif, system-ui, sans-serif; +} + +.no-scrollbar { + scrollbar-width: none; +} + +.no-scrollbar::-webkit-scrollbar { + width: 0; +} diff --git a/pkg/views/src/components/AppLoader.tsx b/pkg/views/src/components/AppLoader.tsx deleted file mode 100644 index b17de46..0000000 --- a/pkg/views/src/components/AppLoader.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { ReactNode, useEffect } from "react"; -import { useWellKnown } from "@/stores/wellKnown.tsx"; -import { useUserinfo } from "@/stores/userinfo.tsx"; - -export default function AppLoader({ children }: { children: ReactNode }) { - const { readWellKnown } = useWellKnown(); - const { readProfiles } = useUserinfo(); - - useEffect(() => { - Promise.all([readWellKnown(), readProfiles()]); - }, []); - - return children; -} \ No newline at end of file diff --git a/pkg/views/src/components/AppShell.tsx b/pkg/views/src/components/AppShell.tsx deleted file mode 100644 index 1de91bc..0000000 --- a/pkg/views/src/components/AppShell.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { - AppBar, - Avatar, - Box, - IconButton, - Slide, - Toolbar, - Typography, - useMediaQuery, - useScrollTrigger -} from "@mui/material"; -import { ReactElement, ReactNode, useEffect, useRef, useState } from "react"; -import { SITE_NAME } from "@/consts"; -import { Link } from "react-router-dom"; -import NavigationMenu, { AppNavigationHeader, isMobileQuery } from "@/components/NavigationMenu.tsx"; -import AccountCircleIcon from "@mui/icons-material/AccountCircleOutlined"; -import { useUserinfo } from "@/stores/userinfo.tsx"; - -function HideOnScroll(props: { window?: () => Window; children: ReactElement }) { - const { children, window } = props; - const trigger = useScrollTrigger({ - target: window ? window() : undefined - }); - - return ( - - {children} - - ); -} - -export default function AppShell({ children }: { children: ReactNode }) { - let documentWindow: Window; - - const { userinfo } = useUserinfo(); - - const isMobile = useMediaQuery(isMobileQuery); - const [open, setOpen] = useState(false); - - useEffect(() => { - documentWindow = window; - }, []); - - const container = useRef(null); - - return ( - <> - documentWindow}> - - - - Logo - - - - {SITE_NAME} - - - setOpen(true)} - sx={{ mr: 1 }} - > - - - - - - - - - - - - {children} - - - setOpen(false)} /> - - ); -} diff --git a/pkg/views/src/components/NavigationMenu.tsx b/pkg/views/src/components/NavigationMenu.tsx deleted file mode 100644 index b00a387..0000000 --- a/pkg/views/src/components/NavigationMenu.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { Collapse, Divider, ListItemIcon, ListItemText, Menu, MenuItem, styled } from "@mui/material"; -import { theme } from "@/theme"; -import { Fragment, ReactNode, useState } from "react"; -import HowToRegIcon from "@mui/icons-material/HowToReg"; -import LoginIcon from "@mui/icons-material/Login"; -import FaceIcon from "@mui/icons-material/Face"; -import LogoutIcon from "@mui/icons-material/ExitToApp"; -import ExpandLess from "@mui/icons-material/ExpandLess"; -import ExpandMore from "@mui/icons-material/ExpandMore"; -import { useUserinfo } from "@/stores/userinfo.tsx"; -import { PopoverProps } from "@mui/material/Popover"; -import { Link } from "react-router-dom"; - -export interface NavigationItem { - icon?: ReactNode; - title?: string; - link?: string; - divider?: boolean; - children?: NavigationItem[]; -} - -export const DRAWER_WIDTH = 320; - -export const AppNavigationHeader = styled("div")(({ theme }) => ({ - display: "flex", - alignItems: "center", - padding: theme.spacing(0, 1), - justifyContent: "flex-start", - height: 64, - ...theme.mixins.toolbar -})); - -export function AppNavigationSection({ items, depth }: { items: NavigationItem[], depth?: number }) { - const [open, setOpen] = useState(false); - - return items.map((item, idx) => { - if (item.divider) { - return ; - } else if (item.children) { - return ( - - setOpen(!open)} sx={{ pl: 2 + (depth ?? 0) * 2, width: 180 }}> - {item.icon} - - {open ? : } - - - - - - ); - } else { - return ( - - - {item.icon} - - - - ); - } - }); -} - -export function AppNavigation() { - const { checkLoggedIn } = useUserinfo(); - - const nav: NavigationItem[] = [ - ...( - checkLoggedIn() ? - [ - { icon: , title: "Account", link: "/users" }, - { divider: true }, - { icon: , title: "Sign out", link: "/auth/sign-out" } - ] : - [ - { icon: , title: "Sign up", link: "/auth/sign-up" }, - { icon: , title: "Sign in", link: "/auth/sign-in" } - ] - ) - ]; - - return ; -} - -export const isMobileQuery = theme.breakpoints.down("md"); - -export default function NavigationMenu({ anchorEl, open, onClose }: { - anchorEl: PopoverProps["anchorEl"]; - open: boolean; - onClose: () => void -}) { - return ( - - - - ); -} diff --git a/pkg/views/src/consts.tsx b/pkg/views/src/consts.tsx deleted file mode 100644 index 461c2ca..0000000 --- a/pkg/views/src/consts.tsx +++ /dev/null @@ -1 +0,0 @@ -export const SITE_NAME = "Goatpass"; diff --git a/pkg/views/src/error.tsx b/pkg/views/src/error.tsx deleted file mode 100644 index 8be7cde..0000000 --- a/pkg/views/src/error.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Link as RouterLink, useRouteError } from "react-router-dom"; -import { Box, Container, Link, Typography } from "@mui/material"; - -export default function ErrorBoundary() { - const error = useRouteError() as any; - - return ( - - - {error.status} - {error?.message ?? "Something went wrong"} - - Back to homepage - - - ); -} \ No newline at end of file diff --git a/pkg/views/src/index.css b/pkg/views/src/index.css deleted file mode 100644 index e69de29..0000000 diff --git a/pkg/views/src/index.vue b/pkg/views/src/index.vue new file mode 100644 index 0000000..4f21c35 --- /dev/null +++ b/pkg/views/src/index.vue @@ -0,0 +1,5 @@ + diff --git a/pkg/views/src/layouts/master.vue b/pkg/views/src/layouts/master.vue new file mode 100644 index 0000000..e7955c7 --- /dev/null +++ b/pkg/views/src/layouts/master.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/pkg/views/src/main.ts b/pkg/views/src/main.ts new file mode 100644 index 0000000..b3665d3 --- /dev/null +++ b/pkg/views/src/main.ts @@ -0,0 +1,54 @@ +import "virtual:uno.css" + +import "./assets/utils.css" + +import { createApp } from "vue" +import { createPinia } from "pinia" + +import "vuetify/styles" +import { createVuetify } from "vuetify" +import { md3 } from "vuetify/blueprints" +import * as components from "vuetify/components" +import * as labsComponents from "vuetify/labs/components" +import * as directives from "vuetify/directives" + +import "@mdi/font/css/materialdesignicons.min.css" +import "@fontsource/roboto/latin.css" +import "@unocss/reset/tailwind.css" + +import index from "./index.vue" +import router from "./router" + +const app = createApp(index) + +app.use( + createVuetify({ + directives, + components: { + ...components, + ...labsComponents, + }, + blueprint: md3, + theme: { + defaultTheme: "original", + themes: { + original: { + colors: { + primary: "#4a5099", + secondary: "#2196f3", + accent: "#009688", + error: "#f44336", + warning: "#ff9800", + info: "#03a9f4", + success: "#4caf50", + }, + }, + }, + }, + }), +) + +app.use(createPinia()) +app.use(router) + +app.mount("#app") diff --git a/pkg/views/src/main.tsx b/pkg/views/src/main.tsx deleted file mode 100644 index 4d8c499..0000000 --- a/pkg/views/src/main.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom"; -import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; -import { LocalizationProvider } from "@mui/x-date-pickers"; -import { CssBaseline, ThemeProvider } from "@mui/material"; -import { theme } from "@/theme.ts"; - -import "virtual:uno.css"; - -import "./index.css"; -import "@unocss/reset/tailwind.css"; -import "@fontsource/roboto/latin.css"; - -import AppShell from "@/components/AppShell.tsx"; -import ErrorBoundary from "@/error.tsx"; -import AppLoader from "@/components/AppLoader.tsx"; -import UserLayout from "@/pages/users/layout.tsx"; -import { UserinfoProvider } from "@/stores/userinfo.tsx"; -import { WellKnownProvider } from "@/stores/wellKnown.tsx"; -import AuthLayout from "@/pages/auth/layout.tsx"; -import AuthGuard from "@/pages/guard.tsx"; - -declare const __GARFISH_EXPORTS__: { - provider: Object; - registerProvider?: (provider: any) => void; -}; - -declare global { - interface Window { - __LAUNCHPAD_TARGET__?: string; - } -} - -const router = createBrowserRouter([ - { - path: "/", - element: , - errorElement: , - children: [ - { path: "/", lazy: () => import("@/pages/landing.tsx") }, - { - path: "/", - element: , - children: [ - { - path: "/users", - element: , - children: [ - { path: "/users", lazy: () => import("@/pages/users/dashboard.tsx") }, - { path: "/users/notifications", lazy: () => import("@/pages/users/notifications.tsx") }, - { path: "/users/personalize", lazy: () => import("@/pages/users/personalize.tsx") }, - { path: "/users/security", lazy: () => import("@/pages/users/security.tsx") } - ] - } - ] - } - ] - }, - { - path: "/auth", - element: , - errorElement: , - children: [ - { path: "/auth/sign-up", errorElement: , lazy: () => import("@/pages/auth/sign-up.tsx") }, - { path: "/auth/sign-in", errorElement: , lazy: () => import("@/pages/auth/sign-in.tsx") }, - { path: "/auth/sign-out", errorElement: , lazy: () => import("@/pages/auth/sign-out.tsx") }, - { path: "/auth/o/connect", errorElement: , lazy: () => import("@/pages/auth/connect.tsx") } - ] - } -]); - -const element = ( - - - - - - - - - - - - - - -); - -ReactDOM.createRoot(document.getElementById("root")!).render(element); \ No newline at end of file diff --git a/pkg/views/src/pages/auth/connect.tsx b/pkg/views/src/pages/auth/connect.tsx deleted file mode 100644 index ce69c4e..0000000 --- a/pkg/views/src/pages/auth/connect.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { useEffect, useState } from "react"; -import { - Alert, - Avatar, - Box, - Button, - Card, - CardContent, - Collapse, - Grid, - LinearProgress, - Typography -} from "@mui/material"; -import { request } from "@/scripts/request.ts"; -import { useUserinfo } from "@/stores/userinfo.tsx"; -import { useSearchParams } from "react-router-dom"; -import OutletIcon from "@mui/icons-material/Outlet"; -import WhatshotIcon from "@mui/icons-material/Whatshot"; - -export function Component() { - const { getAtk } = useUserinfo(); - - const [panel, setPanel] = useState(0); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - const [client, setClient] = useState(null); - - const [searchParams] = useSearchParams(); - - async function preconnect() { - const res = await request(`/api/auth/o/connect${location.search}`, { - headers: { "Authorization": `Bearer ${getAtk()}` } - }); - - if (res.status !== 200) { - setError(await res.text()); - } else { - const data = await res.json(); - - if (data["session"]) { - setPanel(1); - redirect(data["session"]); - } else { - setClient(data["client"]); - setLoading(false); - } - } - } - - useEffect(() => { - preconnect().then(() => console.log("Fetched metadata")); - }, []); - - function decline() { - if (window.history.length > 0) { - window.history.back(); - } else { - window.close(); - } - } - - async function approve() { - setLoading(true); - - const res = await request("/api/auth/o/connect?" + new URLSearchParams({ - client_id: searchParams.get("client_id") as string, - redirect_uri: encodeURIComponent(searchParams.get("redirect_uri") as string), - response_type: "code", - scope: searchParams.get("scope") as string - }), { - method: "POST", - headers: { "Authorization": `Bearer ${getAtk()}` } - }); - - if (res.status !== 200) { - setError(await res.text()); - setLoading(false); - } else { - const data = await res.json(); - setPanel(1); - setTimeout(() => redirect(data["session"]), 1850); - } - } - - function redirect(session: any) { - const url = `${searchParams.get("redirect_uri")}?code=${session["grant_token"]}&state=${searchParams.get("state")}`; - window.open(url, "_self"); - } - - const elements = [ - ( - <> - - - - - Sign in to {client?.name} - - - - - About this app - {client?.description} - - - Make you trust this app - - After you click Approve button, you will share your basic personal information to this application - developer. Some of them will leak your data. Think twice. - - - - - - - - - - - - ), - ( - <> - - - - - Authorized - - - - - Now Redirecting... - Hold on a second, we are going to redirect you to the target. - - - - - ) - ]; - - return ( - <> - {error && {error}} - - - - - - - - {elements[panel]} - - - - ); -} \ No newline at end of file diff --git a/pkg/views/src/pages/auth/layout.tsx b/pkg/views/src/pages/auth/layout.tsx deleted file mode 100644 index f9dec4c..0000000 --- a/pkg/views/src/pages/auth/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Box } from "@mui/material"; -import { Outlet } from "react-router-dom"; - -export default function AuthLayout() { - return ( - - - - - - ) -} \ No newline at end of file diff --git a/pkg/views/src/pages/auth/sign-in.tsx b/pkg/views/src/pages/auth/sign-in.tsx deleted file mode 100644 index 6c43587..0000000 --- a/pkg/views/src/pages/auth/sign-in.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import { Link as RouterLink, useNavigate, useSearchParams } from "react-router-dom"; -import { - Alert, - Avatar, - Box, - Button, - Card, - CardContent, - Collapse, - Grid, - LinearProgress, - Link, - Paper, - TextField, - ToggleButton, - ToggleButtonGroup, - Typography -} from "@mui/material"; -import { FormEvent, useState } from "react"; -import { request } from "@/scripts/request.ts"; -import { useUserinfo } from "@/stores/userinfo.tsx"; -import LoginIcon from "@mui/icons-material/Login"; -import SecurityIcon from "@mui/icons-material/Security"; -import KeyIcon from "@mui/icons-material/Key"; -import PasswordIcon from "@mui/icons-material/Password"; -import EmailIcon from "@mui/icons-material/Email"; - -export function Component() { - const [panel, setPanel] = useState(0); - - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - const [factor, setFactor] = useState(); - const [factorType, setFactorType] = useState(); - - const [factors, setFactors] = useState(null); - const [challenge, setChallenge] = useState(null); - - const { readProfiles } = useUserinfo(); - - const [searchParams] = useSearchParams(); - const navigate = useNavigate(); - - const handlers: any[] = [ - async (evt: FormEvent) => { - evt.preventDefault(); - - const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement)); - if (!data.id) return; - - setLoading(true); - const res = await request("/api/auth", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data) - }); - if (res.status !== 200) { - setError(await res.text()); - } else { - const data = await res.json(); - setFactors(data["factors"]); - setChallenge(data["challenge"]); - setPanel(1); - setError(null); - } - setLoading(false); - }, - async (evt: FormEvent) => { - evt.preventDefault(); - - if (!factor) return; - - setLoading(true); - const res = await request(`/api/auth/factors/${factor}`, { - method: "POST" - }); - if (res.status !== 200 && res.status !== 204) { - setError(await res.text()); - } else { - const item = factors.find((item: any) => item.id === factor).type; - setError(null); - setPanel(2); - setFactorType(factorTypes[item]); - } - setLoading(false); - }, - async (evt: SubmitEvent) => { - evt.preventDefault(); - - const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement)); - if (!data.credentials) return; - - setLoading(true); - const res = await request(`/api/auth`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - challenge_id: challenge?.id, - factor_id: factor, - secret: data.credentials - }) - }); - if (res.status !== 200) { - setError(await res.text()); - } else { - const data = await res.json(); - if (data["is_finished"]) { - await grantToken(data["session"]["grant_token"]); - await readProfiles(); - callback(); - } else { - setError(null); - setPanel(1); - setFactor(undefined); - setFactorType(undefined); - setChallenge(data["challenge"]); - } - } - setLoading(false); - } - ]; - - function callback() { - if (searchParams.has("closable")) { - window.close(); - } else if (searchParams.has("redirect_uri")) { - window.open(searchParams.get("redirect_uri") ?? "/", "_self"); - } else { - navigate("/users"); - } - } - - function getFactorAvailable(factor: any) { - const blacklist: number[] = challenge?.blacklist_factors ?? []; - return blacklist.includes(factor.id); - } - - const factorTypes = [ - { icon: , label: "Password Verification", autoComplete: "password" }, - { icon: , label: "Email One Time Password", autoComplete: "one-time-code" } - ]; - - const elements = [ - ( - <> - - - - - Welcome back - - - - - - - - - - - ), - ( - <> - - - - - Verify that's you - - - - - setFactor(val)} - > - {factors?.map((item: any, idx: number) => ( - - - - {factorTypes[item.type]?.icon} - - - {factorTypes[item.type]?.label} - - - - ))} - - - - - - - ), - ( - <> - - - - - Enter the credentials - - - - - - - - - - - ) - ]; - - async function grantToken(tk: string) { - const res = await request("/api/auth/token", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - code: tk, - grant_type: "grant_token" - }) - }); - if (res.status !== 200) { - const err = await res.text(); - setError(err); - throw new Error(err); - } else { - setError(null); - } - } - - return ( - <> - {error && {error}} - - - - You need sign in before take an action. After that, we will take you back to your work. - - - - - - - - - - {elements[panel]} - - - - - - - Risk {challenge?.risk_level}  - Progress {challenge?.progress}/{challenge?.requirements} - - - - - - - - - - - Haven't an account? Sign up! - - - - - ); -} \ No newline at end of file diff --git a/pkg/views/src/pages/auth/sign-out.tsx b/pkg/views/src/pages/auth/sign-out.tsx deleted file mode 100644 index 6c37a8d..0000000 --- a/pkg/views/src/pages/auth/sign-out.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Avatar, Button, Card, CardContent, Typography } from "@mui/material"; -import { useUserinfo } from "@/stores/userinfo.tsx"; -import LogoutIcon from "@mui/icons-material/Logout"; -import { useNavigate } from "react-router-dom"; - -export function Component() { - const { clearUserinfo } = useUserinfo(); - - const navigate = useNavigate(); - - async function signout() { - clearUserinfo(); - navigate("/"); - } - - return ( - <> - - - - - - - Sign out - - Sign out will clear your data on this device. Also will affected those use union identification services. - You need sign in again get access them. - - - - - - - ); -} \ No newline at end of file diff --git a/pkg/views/src/pages/auth/sign-up.tsx b/pkg/views/src/pages/auth/sign-up.tsx deleted file mode 100644 index b57f5d1..0000000 --- a/pkg/views/src/pages/auth/sign-up.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import UserIcon from "@mui/icons-material/PersonAddAlt1"; -import HowToRegIcon from "@mui/icons-material/HowToReg"; -import { Link as RouterLink, useNavigate, useSearchParams } from "react-router-dom"; -import { - Alert, - Avatar, - Box, - Button, - Card, - CardContent, - Checkbox, - Collapse, - FormControlLabel, - Grid, - LinearProgress, - Link, - TextField, - Typography -} from "@mui/material"; -import { FormEvent, useState } from "react"; -import { request } from "@/scripts/request.ts"; -import { useWellKnown } from "@/stores/wellKnown.tsx"; - -export function Component() { - const [done, setDone] = useState(false); - - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - const { wellKnown } = useWellKnown(); - - const [searchParams] = useSearchParams(); - const navigate = useNavigate(); - - async function submit(evt: FormEvent) { - evt.preventDefault(); - - const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement)); - if (!data.human_verification) return; - if (!data.name || !data.nick || !data.email || !data.password) return; - - setLoading(true); - const res = await request("/api/users", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data) - }); - if (res.status !== 200) { - setError(await res.text()); - } else { - setError(null); - setDone(true); - } - setLoading(false); - } - - function callback() { - if (searchParams.has("closable")) { - window.close(); - } else { - navigate("/auth/sign-in"); - } - } - - const elements = [ - ( - <> - - - - - Create an account - - - - - - - - - - - - - - - - { - !wellKnown?.open_registration && - - - } - - } - label={"I'm not a robot."} - /> - - - - - - ), - ( - <> - - - - - Congratulations! - - Your account has been created and activation email has sent to your inbox! - - - - callback()} className="cursor-pointer">Go login - - - - After you login, then you can take part in the entire smartsheep community. - - - ) - ]; - - return ( - <> - {error && {error}} - - - - - - - - {!done ? elements[0] : elements[1]} - - - - - - - Already have an account? Sign in! - - - - - ); -} \ No newline at end of file diff --git a/pkg/views/src/pages/guard.tsx b/pkg/views/src/pages/guard.tsx deleted file mode 100644 index ce39a4c..0000000 --- a/pkg/views/src/pages/guard.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect } from "react"; -import { Box, CircularProgress } from "@mui/material"; -import { Outlet, useLocation, useNavigate } from "react-router-dom"; -import { useUserinfo } from "@/stores/userinfo.tsx"; - -export default function AuthGuard() { - const { userinfo } = useUserinfo(); - - const navigate = useNavigate(); - const location = useLocation(); - - useEffect(() => { - console.log(userinfo) - if (userinfo?.isReady) { - if (!userinfo?.isLoggedIn) { - const callback = location.pathname + location.search; - navigate({ pathname: "/auth/sign-in", search: `redirect_uri=${callback}` }); - } - } - }, [userinfo]); - - return !userinfo?.isReady ? ( - - - - - - ) : ; -} \ No newline at end of file diff --git a/pkg/views/src/pages/landing.tsx b/pkg/views/src/pages/landing.tsx deleted file mode 100644 index 1d4d998..0000000 --- a/pkg/views/src/pages/landing.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Button, Container, Grid, Typography } from "@mui/material"; -import { Link as RouterLink } from "react-router-dom"; - -export function Component() { - return ( - - - - All Goatworks® Services - In a single account - - That's - Goatpass - - - - Logo - - - - ); -} \ No newline at end of file diff --git a/pkg/views/src/pages/users/dashboard.tsx b/pkg/views/src/pages/users/dashboard.tsx deleted file mode 100644 index 6ab9ba7..0000000 --- a/pkg/views/src/pages/users/dashboard.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Alert, Box, Card, CardContent, Container, Typography } from "@mui/material"; -import { useUserinfo } from "@/stores/userinfo.tsx"; - -export function Component() { - const { userinfo } = useUserinfo(); - - return ( - - - Welcome, {userinfo?.displayName} - What can I help you today? - - - { - !userinfo?.data?.confirmed_at && - - Your account haven't confirmed yet. Go to your linked email - inbox and check out our registration confirm email. - - } - - - Frequently Asked Questions - - - - 没有人有问题。没有人敢有问题。鲁迅曾经说过: - 解决不了问题,就解决提问题的人。 —— 鲁迅 - 所以,我们的客诉率是 0% 哦~ - - - - - ); -} \ No newline at end of file diff --git a/pkg/views/src/pages/users/layout.tsx b/pkg/views/src/pages/users/layout.tsx deleted file mode 100644 index df6147c..0000000 --- a/pkg/views/src/pages/users/layout.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Outlet, useLocation, useNavigate } from "react-router-dom"; -import { Box, Tab, Tabs, useMediaQuery } from "@mui/material"; -import { useEffect, useState } from "react"; -import { theme } from "@/theme.ts"; -import DashboardIcon from "@mui/icons-material/Dashboard"; -import InboxIcon from "@mui/icons-material/Inbox"; -import DrawIcon from "@mui/icons-material/Draw"; -import SecurityIcon from "@mui/icons-material/Security"; - -export default function UserLayout() { - const [focus, setFocus] = useState(0); - - const isMobile = useMediaQuery(theme.breakpoints.down("md")); - - const locations = ["/users", "/users/notifications", "/users/personalize", "/users/security"]; - const tabs = [ - { icon: , label: "Dashboard" }, - { icon: , label: "Notifications" }, - { icon: , label: "Personalize" }, - { icon: , label: "Security" } - ]; - - const location = useLocation(); - const navigate = useNavigate(); - - useEffect(() => { - const idx = locations.indexOf(location.pathname); - setFocus(idx); - }, []); - - function swap(idx: number) { - navigate(locations[idx]); - setFocus(idx); - } - - return ( - - - swap(val)} - sx={{ - borderRight: isMobile ? 0 : 1, - borderBottom: isMobile ? 1 : 0, - borderColor: "divider", - height: isMobile ? "fit-content" : "100%", - py: isMobile ? 0 : 1, - px: isMobile ? 1 : 0 - }} - > - {tabs.map((tab, idx) => ( - - ))} - - - - - - - - ); -} \ No newline at end of file diff --git a/pkg/views/src/pages/users/notifications.tsx b/pkg/views/src/pages/users/notifications.tsx deleted file mode 100644 index 95814da..0000000 --- a/pkg/views/src/pages/users/notifications.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Alert, Box, Collapse, IconButton, LinearProgress, List, ListItem, ListItemText } from "@mui/material"; -import { useUserinfo } from "@/stores/userinfo.tsx"; -import { request } from "@/scripts/request.ts"; -import { useEffect, useState } from "react"; -import { TransitionGroup } from "react-transition-group"; -import MarkEmailReadIcon from "@mui/icons-material/MarkEmailRead"; - -export function Component() { - const { userinfo, readProfiles, getAtk } = useUserinfo(); - - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const [notifications, setNotifications] = useState([]); - - async function readNotifications() { - const res = await request(`/api/notifications?take=100`, { - headers: { Authorization: `Bearer ${getAtk()}` } - }); - if (res.status !== 200) { - setError(await res.text()); - } else { - const data = await res.json(); - setNotifications(data["data"]); - setError(null); - } - } - - async function markNotifications(item: any) { - setLoading(true); - const res = await request(`/api/notifications/${item.id}/read`, { - method: "PUT", - headers: { Authorization: `Bearer ${getAtk()}` } - }); - if (res.status !== 200) { - setError(await res.text()); - } else { - readNotifications().then(() => readProfiles()); - setError(null); - } - setLoading(false); - } - - useEffect(() => { - readNotifications().then(() => setLoading(false)); - }, []); - - return ( - - - - - - - {error} - - - - You are done! There's no unread notifications for you. - - - - - {notifications.map((item, idx) => ( - - markNotifications(item)} - > - - - }> - - - - ))} - - - - ); -} \ No newline at end of file diff --git a/pkg/views/src/pages/users/personalize.tsx b/pkg/views/src/pages/users/personalize.tsx deleted file mode 100644 index fa9f02a..0000000 --- a/pkg/views/src/pages/users/personalize.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import { - Alert, - Avatar, - Box, - Button, - Card, - CardContent, - CircularProgress, - Collapse, - Container, - Divider, - Grid, - LinearProgress, - Snackbar, - styled, - TextField, - Typography -} from "@mui/material"; -import { useUserinfo } from "@/stores/userinfo.tsx"; -import { ChangeEvent, FormEvent, useState } from "react"; -import { DatePicker } from "@mui/x-date-pickers"; -import { request } from "@/scripts/request.ts"; -import SaveIcon from "@mui/icons-material/Save"; -import PublishIcon from "@mui/icons-material/Publish"; -import NoAccountsIcon from "@mui/icons-material/NoAccounts"; -import dayjs from "dayjs"; - -const VisuallyHiddenInput = styled("input")({ - clip: "rect(0 0 0 0)", - clipPath: "inset(50%)", - height: 1, - overflow: "hidden", - position: "absolute", - bottom: 0, - left: 0, - whiteSpace: "nowrap", - width: 1 -}); - -export function Component() { - const { userinfo, readProfiles, getAtk } = useUserinfo(); - - const [done, setDone] = useState(false); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - async function submit(evt: FormEvent) { - evt.preventDefault(); - - const data: any = Object.fromEntries(new FormData(evt.target as HTMLFormElement)); - if (data.birthday) data.birthday = new Date(data.birthday); - - setLoading(true); - const res = await request("/api/users/me", { - method: "PUT", - headers: { "Content-Type": "application/json", "Authorization": `Bearer ${getAtk()}` }, - body: JSON.stringify(data) - }); - if (res.status !== 200) { - setError(await res.text()); - } else { - await readProfiles(); - setDone(true); - setError(null); - } - setLoading(false); - } - - async function changeAvatar(evt: ChangeEvent) { - if (!evt.target.files) return; - - const file = evt.target.files[0]; - const payload = new FormData(); - payload.set("avatar", file); - - setLoading(true); - const res = await request("/api/avatar", { - method: "PUT", - headers: { "Authorization": `Bearer ${getAtk()}` }, - body: payload - }); - if (res.status !== 200) { - setError(await res.text()); - } else { - await readProfiles(); - setDone(true); - setError(null); - } - setLoading(false); - } - - function getBirthday() { - return userinfo?.data?.profile?.birthday ? dayjs(userinfo?.data?.profile?.birthday) : undefined; - } - - const basisForm = ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/* @ts-ignore */} - - - - - ); - - return ( - - - Personalize - - Customize your appearance and name card across all Goatworks information. - - - - - {error} - - - - - - - - - - - Information - - The information for public. Let us and others better to know who you are. - - - - { - userinfo?.data != null ? basisForm : - - - - } - - - - - - setDone(false)} - message="Your profile has been updated. Some settings maybe need sometime to apply across site." - /> - - ); -} \ No newline at end of file diff --git a/pkg/views/src/pages/users/security.tsx b/pkg/views/src/pages/users/security.tsx deleted file mode 100644 index f978590..0000000 --- a/pkg/views/src/pages/users/security.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import { - Alert, - Box, - Card, - CardContent, - Collapse, - Container, - Grid, - LinearProgress, - Tab, - Tabs, - Typography -} from "@mui/material"; -import { useUserinfo } from "@/stores/userinfo.tsx"; -import { TabContext, TabPanel } from "@mui/lab"; -import { useEffect, useState } from "react"; -import { DataGrid, GridActionsCellItem, GridColDef, GridRowParams, GridValueGetterParams } from "@mui/x-data-grid"; -import { request } from "@/scripts/request.ts"; -import ExitToAppIcon from "@mui/icons-material/ExitToApp"; - - -export function Component() { - const dataDefinitions: { [id: string]: GridColDef[] } = { - challenges: [ - { field: "id", headerName: "ID", width: 64 }, - { field: "ip_address", headerName: "IP Address", minWidth: 128 }, - { field: "user_agent", headerName: "User Agent", minWidth: 320 }, - { - field: "created_at", - headerName: "Issued At", - minWidth: 160, - valueGetter: (params: GridValueGetterParams) => new Date(params.row.created_at).toLocaleString() - } - ], - sessions: [ - { field: "id", headerName: "ID", width: 64 }, - { - field: "audiences", - headerName: "Audiences", - minWidth: 128, - valueGetter: (params: GridValueGetterParams) => params.row.audiences.join(", ") - }, - { - field: "claims", - headerName: "Claims", - minWidth: 224, - valueGetter: (params: GridValueGetterParams) => params.row.claims.join(", ") - }, - { - field: "created_at", - headerName: "Issued At", - minWidth: 160, - valueGetter: (params: GridValueGetterParams) => new Date(params.row.created_at).toLocaleString() - }, - { - field: "actions", - type: "actions", - getActions: (params: GridRowParams) => [ - } - onClick={() => killSession(params.row)} - disabled={loading} - label="Sign Out" - /> - ] - } - ], - events: [ - { field: "id", headerName: "ID", width: 64 }, - { field: "type", headerName: "Type", minWidth: 128 }, - { field: "target", headerName: "Affected Object", minWidth: 128 }, - { field: "ip_address", headerName: "IP Address", minWidth: 128 }, - { field: "user_agent", headerName: "User Agent", minWidth: 128 }, - { - field: "created_at", - headerName: "Performed At", - minWidth: 160, - valueGetter: (params: GridValueGetterParams) => new Date(params.row.created_at).toLocaleString() - } - ] - }; - - const { getAtk } = useUserinfo(); - - const [challenges, setChallenges] = useState([]); - const [challengeCount, setChallengeCount] = useState(0); - const [sessions, setSessions] = useState([]); - const [sessionCount, setSessionCount] = useState(0); - const [events, setEvents] = useState([]); - const [eventCount, setEventCount] = useState(0); - - const [pagination, setPagination] = useState({ - challenges: { page: 0, pageSize: 5 }, - sessions: { page: 0, pageSize: 5 }, - events: { page: 0, pageSize: 5 } - }); - - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - const [reverting] = useState({ - challenges: true, - sessions: true, - events: true - }); - - const [dataPane, setDataPane] = useState("challenges"); - - async function readChallenges() { - reverting.challenges = true; - const res = await request("/api/users/me/challenges?" + new URLSearchParams({ - take: pagination.challenges.pageSize.toString(), - offset: (pagination.challenges.page * pagination.challenges.pageSize).toString() - }), { - headers: { Authorization: `Bearer ${getAtk()}` } - }); - if (res.status !== 200) { - setError(await res.text()); - } else { - const data = await res.json(); - setChallenges(data["data"]); - setChallengeCount(data["count"]); - } - reverting.challenges = false; - } - - async function readSessions() { - reverting.sessions = true; - const res = await request("/api/users/me/sessions?" + new URLSearchParams({ - take: pagination.sessions.pageSize.toString(), - offset: (pagination.sessions.page * pagination.sessions.pageSize).toString() - }), { - headers: { Authorization: `Bearer ${getAtk()}` } - }); - if (res.status !== 200) { - setError(await res.text()); - } else { - const data = await res.json(); - setSessions(data["data"]); - setSessionCount(data["count"]); - } - reverting.sessions = false; - } - - async function readEvents() { - reverting.events = true; - const res = await request("/api/users/me/events?" + new URLSearchParams({ - take: pagination.events.pageSize.toString(), - offset: (pagination.events.page * pagination.events.pageSize).toString() - }), { - headers: { Authorization: `Bearer ${getAtk()}` } - }); - if (res.status !== 200) { - setError(await res.text()); - } else { - const data = await res.json(); - setEvents(data["data"]); - setEventCount(data["count"]); - } - reverting.events = false; - } - - async function killSession(item: any) { - setLoading(true); - const res = await request(`/api/users/me/sessions/${item.id}`, { - method: "DELETE", - headers: { Authorization: `Bearer ${getAtk()}` } - }); - if (res.status !== 200) { - setError(await res.text()); - } else { - await readSessions(); - setError(null); - } - setLoading(false); - } - - useEffect(() => { - readChallenges().then(() => console.log("Refreshed challenges list.")); - }, [pagination.challenges]); - - useEffect(() => { - readSessions().then(() => console.log("Refreshed sessions list.")); - }, [pagination.sessions]); - - useEffect(() => { - readEvents().then(() => console.log("Refreshed events list.")); - }, [pagination.events]); - - return ( - - - Security - - Overview and control all security details in your account. - - - - - {error} - - - - - - - - - - - - - setDataPane(val)}> - - - - - - - - - setPagination({ ...pagination, challenges: val })} - checkboxSelection - /> - - - setPagination({ ...pagination, sessions: val })} - checkboxSelection - /> - - - setPagination({ ...pagination, events: val })} - checkboxSelection - /> - - - - - - - - - ); -} \ No newline at end of file diff --git a/pkg/views/src/router/index.ts b/pkg/views/src/router/index.ts new file mode 100644 index 0000000..994d293 --- /dev/null +++ b/pkg/views/src/router/index.ts @@ -0,0 +1,15 @@ +import { createRouter, createWebHistory } from "vue-router" +import MasterLayout from "@/layouts/master.vue" + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: "/", + component: MasterLayout, + children: [{ path: "/", name: "dashboard", component: () => import("@/views/dashboard.vue") }], + }, + ], +}) + +export default router diff --git a/pkg/views/src/scripts/request.ts b/pkg/views/src/scripts/request.ts index b85771b..5540ff2 100644 --- a/pkg/views/src/scripts/request.ts +++ b/pkg/views/src/scripts/request.ts @@ -1,4 +1,10 @@ +declare global { + interface Window { + __LAUNCHPAD_TARGET__?: string + } +} + export async function request(input: string, init?: RequestInit) { - const prefix = window.__LAUNCHPAD_TARGET__ ?? ""; + const prefix = window.__LAUNCHPAD_TARGET__ ?? "" return await fetch(prefix + input, init) -} \ No newline at end of file +} diff --git a/pkg/views/src/stores/userinfo.ts b/pkg/views/src/stores/userinfo.ts new file mode 100644 index 0000000..2f4f1a8 --- /dev/null +++ b/pkg/views/src/stores/userinfo.ts @@ -0,0 +1,56 @@ +import Cookie from "universal-cookie" +import { defineStore } from "pinia" +import { ref } from "vue" +import { request } from "@/scripts/request" + +export interface Userinfo { + isReady: boolean + isLoggedIn: boolean + displayName: string + data: any +} + +const defaultUserinfo: Userinfo = { + isReady: false, + isLoggedIn: false, + displayName: "Citizen", + data: null +} + +export function getAtk(): string { + return new Cookie().get("identity_auth_key") +} + +export function checkLoggedIn(): boolean { + return new Cookie().get("identity_auth_key") +} + +export const useUserinfo = defineStore("userinfo", () => { + const userinfo = ref(defaultUserinfo) + const isReady = ref(false) + + async function readProfiles() { + if (!checkLoggedIn()) { + isReady.value = true; + } + + const res = await request("/api/users/me", { + headers: { "Authorization": `Bearer ${getAtk()}` } + }); + + if (res.status !== 200) { + return; + } + + const data = await res.json(); + + userinfo.value = { + isReady: true, + isLoggedIn: true, + displayName: data["nick"], + data: data + }; + } + + return { userinfo, isReady, readProfiles } +}) diff --git a/pkg/views/src/stores/userinfo.tsx b/pkg/views/src/stores/userinfo.tsx deleted file mode 100644 index 4b7aa5c..0000000 --- a/pkg/views/src/stores/userinfo.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import Cookie from "universal-cookie"; -import { request } from "../scripts/request.ts"; -import { createContext, useContext, useState } from "react"; - -export interface Userinfo { - isReady: boolean, - isLoggedIn: boolean, - displayName: string, - data: any, -} - -const defaultUserinfo: Userinfo = { - isReady: false, - isLoggedIn: false, - displayName: "Citizen", - data: null -}; - -const UserinfoContext = createContext({ userinfo: defaultUserinfo }); - -export function UserinfoProvider(props: any) { - const [userinfo, setUserinfo] = useState(structuredClone(defaultUserinfo)); - - function getAtk(): string { - return new Cookie().get("identity_auth_key"); - } - - function checkLoggedIn(): boolean { - return new Cookie().get("identity_auth_key"); - } - - async function readProfiles() { - if (!checkLoggedIn()) { - setUserinfo((data) => { - data.isReady = true; - return data; - }); - } - - const res = await request("/api/users/me", { - headers: { "Authorization": `Bearer ${getAtk()}` } - }); - - if (res.status !== 200) { - return; - } - - const data = await res.json(); - - setUserinfo({ - isReady: true, - isLoggedIn: true, - displayName: data["nick"], - data: data - }); - } - - function clearUserinfo() { - const cookies = document.cookie.split(";"); - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i]; - const eqPos = cookie.indexOf("="); - const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie; - document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; - } - - setUserinfo(defaultUserinfo); - } - - return ( - - {props.children} - - ); -} - -export function useUserinfo() { - return useContext(UserinfoContext); -} \ No newline at end of file diff --git a/pkg/views/src/stores/wellKnown.tsx b/pkg/views/src/stores/wellKnown.tsx deleted file mode 100644 index f3f9d2d..0000000 --- a/pkg/views/src/stores/wellKnown.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { createContext, useContext, useState } from "react"; -import { request } from "../scripts/request.ts"; - -const WellKnownContext = createContext(null); - -export function WellKnownProvider(props: any) { - const [wellKnown, setWellKnown] = useState(null); - - async function readWellKnown() { - const res = await request("/.well-known"); - setWellKnown(await res.json()); - } - - return ( - - {props.children} - - ); -} - -export function useWellKnown() { - return useContext(WellKnownContext); -} \ No newline at end of file diff --git a/pkg/views/src/theme.ts b/pkg/views/src/theme.ts deleted file mode 100644 index 0c38afc..0000000 --- a/pkg/views/src/theme.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { createTheme } from "@mui/material/styles"; - -export const theme = createTheme({ - palette: { - primary: { - main: "#49509e", - }, - secondary: { - main: "#d43630", - }, - }, - typography: { - h1: { fontSize: "2.5rem" }, - h2: { fontSize: "2rem" }, - h3: { fontSize: "1.75rem" }, - h4: { fontSize: "1.5rem" }, - h5: { fontSize: "1.25rem" }, - h6: { fontSize: "1.15rem" }, - }, -}); diff --git a/pkg/views/src/views/dashboard.vue b/pkg/views/src/views/dashboard.vue new file mode 100644 index 0000000..608217b --- /dev/null +++ b/pkg/views/src/views/dashboard.vue @@ -0,0 +1,3 @@ + diff --git a/pkg/views/tsconfig.app.json b/pkg/views/tsconfig.app.json new file mode 100644 index 0000000..e14c754 --- /dev/null +++ b/pkg/views/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/pkg/views/tsconfig.json b/pkg/views/tsconfig.json index 67398cd..66b5e57 100644 --- a/pkg/views/tsconfig.json +++ b/pkg/views/tsconfig.json @@ -1,30 +1,11 @@ { - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - - "baseUrl": "./src", - "paths": { - "@/*": ["./*"] + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" } - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] + ] } diff --git a/pkg/views/tsconfig.node.json b/pkg/views/tsconfig.node.json index 97ede7e..2c669ee 100644 --- a/pkg/views/tsconfig.node.json +++ b/pkg/views/tsconfig.node.json @@ -1,11 +1,13 @@ { + "extends": "@tsconfig/node20/tsconfig.json", + "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"], "compilerOptions": { "composite": true, - "skipLibCheck": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true - }, - "include": ["vite.config.ts"] + "moduleResolution": "Bundler", + "types": ["node"] + } } diff --git a/pkg/views/uno.config.ts b/pkg/views/uno.config.ts index b6816ad..2d323f7 100644 --- a/pkg/views/uno.config.ts +++ b/pkg/views/uno.config.ts @@ -1,5 +1,5 @@ -import { defineConfig, presetUno } from "unocss"; +import { defineConfig, presetAttributify, presetTypography, presetUno } from "unocss" export default defineConfig({ - presets: [presetUno({ preflight: false })] -}); \ No newline at end of file + presets: [presetAttributify(), presetTypography(), presetUno({ preflight: false })], +}) diff --git a/pkg/views/vite.config.ts b/pkg/views/vite.config.ts index b707ccd..9208dfc 100644 --- a/pkg/views/vite.config.ts +++ b/pkg/views/vite.config.ts @@ -1,19 +1,20 @@ -import { defineConfig } from 'vite' -import path from "path"; -import react from '@vitejs/plugin-react-swc' +import { fileURLToPath, URL } from "node:url" + +import { defineConfig } from "vite" +import vue from "@vitejs/plugin-vue" +import vueJsx from "@vitejs/plugin-vue-jsx" import unocss from "unocss/vite" // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react(), unocss()], + plugins: [vue(), vueJsx(), unocss()], resolve: { alias: { - "@": path.resolve(__dirname, "./src"), + "@": fileURLToPath(new URL("./src", import.meta.url)), }, }, server: { proxy: { - "/.well-known": "http://localhost:8444", "/api": "http://localhost:8444" } }