From 920efd30213e0075e238d8957259769385fd0158 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 12 Mar 2024 21:26:00 +0800 Subject: [PATCH 01/13] :tada: Initial New Design Project --- pkg/views/.eslintrc.cjs | 21 +- pkg/views/.gitignore | 8 +- pkg/views/.prettierrc.json | 8 + pkg/views/.vscode/extensions.json | 3 + pkg/views/README.md | 51 +-- pkg/views/bun.lockb | Bin 166649 -> 152389 bytes pkg/views/{src/vite-env.d.ts => env.d.ts} | 0 pkg/views/index.html | 8 +- pkg/views/package.json | 64 ++-- pkg/views/public/favicon.svg | 21 -- pkg/views/src/assets/utils.css | 15 + pkg/views/src/components/AppLoader.tsx | 14 - pkg/views/src/components/AppShell.tsx | 95 ------ pkg/views/src/components/NavigationMenu.tsx | 98 ------ pkg/views/src/consts.tsx | 1 - pkg/views/src/error.tsx | 23 -- pkg/views/src/index.css | 0 pkg/views/src/index.vue | 5 + pkg/views/src/layouts/master.vue | 60 ++++ pkg/views/src/main.ts | 54 ++++ pkg/views/src/main.tsx | 90 ------ pkg/views/src/pages/auth/connect.tsx | 182 ----------- pkg/views/src/pages/auth/layout.tsx | 12 - pkg/views/src/pages/auth/sign-in.tsx | 331 -------------------- pkg/views/src/pages/auth/sign-out.tsx | 50 --- pkg/views/src/pages/auth/sign-up.tsx | 198 ------------ pkg/views/src/pages/guard.tsx | 29 -- pkg/views/src/pages/landing.tsx | 22 -- pkg/views/src/pages/users/dashboard.tsx | 35 --- pkg/views/src/pages/users/layout.tsx | 65 ---- pkg/views/src/pages/users/notifications.tsx | 87 ----- pkg/views/src/pages/users/personalize.tsx | 250 --------------- pkg/views/src/pages/users/security.tsx | 267 ---------------- pkg/views/src/router/index.ts | 15 + pkg/views/src/scripts/request.ts | 10 +- pkg/views/src/stores/userinfo.ts | 56 ++++ pkg/views/src/stores/userinfo.tsx | 79 ----- pkg/views/src/stores/wellKnown.tsx | 23 -- pkg/views/src/theme.ts | 20 -- pkg/views/src/views/dashboard.vue | 3 + pkg/views/tsconfig.app.json | 14 + pkg/views/tsconfig.json | 35 +-- pkg/views/tsconfig.node.json | 14 +- pkg/views/uno.config.ts | 6 +- pkg/views/vite.config.ts | 13 +- 45 files changed, 347 insertions(+), 2108 deletions(-) create mode 100644 pkg/views/.prettierrc.json create mode 100644 pkg/views/.vscode/extensions.json rename pkg/views/{src/vite-env.d.ts => env.d.ts} (100%) delete mode 100755 pkg/views/public/favicon.svg create mode 100644 pkg/views/src/assets/utils.css delete mode 100644 pkg/views/src/components/AppLoader.tsx delete mode 100644 pkg/views/src/components/AppShell.tsx delete mode 100644 pkg/views/src/components/NavigationMenu.tsx delete mode 100644 pkg/views/src/consts.tsx delete mode 100644 pkg/views/src/error.tsx delete mode 100644 pkg/views/src/index.css create mode 100644 pkg/views/src/index.vue create mode 100644 pkg/views/src/layouts/master.vue create mode 100644 pkg/views/src/main.ts delete mode 100644 pkg/views/src/main.tsx delete mode 100644 pkg/views/src/pages/auth/connect.tsx delete mode 100644 pkg/views/src/pages/auth/layout.tsx delete mode 100644 pkg/views/src/pages/auth/sign-in.tsx delete mode 100644 pkg/views/src/pages/auth/sign-out.tsx delete mode 100644 pkg/views/src/pages/auth/sign-up.tsx delete mode 100644 pkg/views/src/pages/guard.tsx delete mode 100644 pkg/views/src/pages/landing.tsx delete mode 100644 pkg/views/src/pages/users/dashboard.tsx delete mode 100644 pkg/views/src/pages/users/layout.tsx delete mode 100644 pkg/views/src/pages/users/notifications.tsx delete mode 100644 pkg/views/src/pages/users/personalize.tsx delete mode 100644 pkg/views/src/pages/users/security.tsx create mode 100644 pkg/views/src/router/index.ts create mode 100644 pkg/views/src/stores/userinfo.ts delete mode 100644 pkg/views/src/stores/userinfo.tsx delete mode 100644 pkg/views/src/stores/wellKnown.tsx delete mode 100644 pkg/views/src/theme.ts create mode 100644 pkg/views/src/views/dashboard.vue create mode 100644 pkg/views/tsconfig.app.json 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 b20726d0f6073c9200167bf7a0af4e3102b00b7f..5f8e77280b47078b6850b19c58ad1a7bea578fa4 100755 GIT binary patch delta 46467 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 delta 56290 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 - + - 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" } } From 95c486b8f41bebbef7730c01e78afb2adb1f1924 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 12 Mar 2024 23:23:16 +0800 Subject: [PATCH 02/13] :sparkles: Sign in --- pkg/services/factors.go | 5 +- pkg/views/src/components/Copyright.vue | 6 + .../src/components/auth/AccountLocator.vue | 59 +++++++++ .../src/components/auth/FactorApplicator.vue | 120 ++++++++++++++++++ .../src/components/auth/FactorPicker.vue | 74 +++++++++++ pkg/views/src/router/index.ts | 11 +- pkg/views/src/views/auth/sign-in.vue | 69 ++++++++++ 7 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 pkg/views/src/components/Copyright.vue create mode 100644 pkg/views/src/components/auth/AccountLocator.vue create mode 100644 pkg/views/src/components/auth/FactorApplicator.vue create mode 100644 pkg/views/src/components/auth/FactorPicker.vue create mode 100644 pkg/views/src/views/auth/sign-in.vue diff --git a/pkg/services/factors.go b/pkg/services/factors.go index 15b066c..21f538a 100644 --- a/pkg/services/factors.go +++ b/pkg/services/factors.go @@ -1,9 +1,10 @@ package services import ( + "fmt" + "code.smartsheep.studio/hydrogen/identity/pkg/database" "code.smartsheep.studio/hydrogen/identity/pkg/models" - "fmt" "github.com/google/uuid" "github.com/spf13/viper" ) @@ -51,7 +52,7 @@ func GetFactorCode(factor models.AuthFactor) (bool, error) { return true, err } - factor.Secret = uuid.NewString()[:8] + factor.Secret = uuid.NewString()[:6] if err := database.C.Save(&factor).Error; err != nil { return true, err } diff --git a/pkg/views/src/components/Copyright.vue b/pkg/views/src/components/Copyright.vue new file mode 100644 index 0000000..f3f6baa --- /dev/null +++ b/pkg/views/src/components/Copyright.vue @@ -0,0 +1,6 @@ + diff --git a/pkg/views/src/components/auth/AccountLocator.vue b/pkg/views/src/components/auth/AccountLocator.vue new file mode 100644 index 0000000..0e6be6c --- /dev/null +++ b/pkg/views/src/components/auth/AccountLocator.vue @@ -0,0 +1,59 @@ + + + diff --git a/pkg/views/src/components/auth/FactorApplicator.vue b/pkg/views/src/components/auth/FactorApplicator.vue new file mode 100644 index 0000000..4571c5a --- /dev/null +++ b/pkg/views/src/components/auth/FactorApplicator.vue @@ -0,0 +1,120 @@ + + + diff --git a/pkg/views/src/components/auth/FactorPicker.vue b/pkg/views/src/components/auth/FactorPicker.vue new file mode 100644 index 0000000..730a5c5 --- /dev/null +++ b/pkg/views/src/components/auth/FactorPicker.vue @@ -0,0 +1,74 @@ + + + diff --git a/pkg/views/src/router/index.ts b/pkg/views/src/router/index.ts index 994d293..d78ea4a 100644 --- a/pkg/views/src/router/index.ts +++ b/pkg/views/src/router/index.ts @@ -7,8 +7,17 @@ const router = createRouter({ { path: "/", component: MasterLayout, - children: [{ path: "/", name: "dashboard", component: () => import("@/views/dashboard.vue") }], + children: [ + { path: "/", name: "dashboard", component: () => import("@/views/dashboard.vue") }, + ], }, + { + path: "/auth", + children: [ + { path: "sign-in", name: "auth.sign-in", component: () => import("@/views/auth/sign-in.vue") }, + // { path: "sign-up", name: "auth.sign-up", component: () => import("@/views/auth/sign-up.vue") }, + ] + } ], }) diff --git a/pkg/views/src/views/auth/sign-in.vue b/pkg/views/src/views/auth/sign-in.vue new file mode 100644 index 0000000..81b30f0 --- /dev/null +++ b/pkg/views/src/views/auth/sign-in.vue @@ -0,0 +1,69 @@ + + + + + +@/components/Copyright.vue From 7d11640dddd76dc93b5aae1637c3b0707472f0b5 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 13 Mar 2024 22:12:08 +0800 Subject: [PATCH 03/13] :sparkles: Sign up --- .../src/components/auth/AccountLocator.vue | 4 +- .../src/components/auth/CallbackNotify.vue | 16 ++ pkg/views/src/layouts/master.vue | 4 +- pkg/views/src/router/index.ts | 2 +- pkg/views/src/views/auth/sign-in.vue | 4 +- pkg/views/src/views/auth/sign-up.vue | 162 ++++++++++++++++++ 6 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 pkg/views/src/components/auth/CallbackNotify.vue create mode 100644 pkg/views/src/views/auth/sign-up.vue diff --git a/pkg/views/src/components/auth/AccountLocator.vue b/pkg/views/src/components/auth/AccountLocator.vue index 0e6be6c..2f42cec 100644 --- a/pkg/views/src/components/auth/AccountLocator.vue +++ b/pkg/views/src/components/auth/AccountLocator.vue @@ -9,7 +9,9 @@ -
+
+ Sign up + +
+ + + You need to sign in before access that page. After you signed in, we will redirect you to:
+ {{ route.query["redirect_uri"] }} +
+
+
+ + + diff --git a/pkg/views/src/layouts/master.vue b/pkg/views/src/layouts/master.vue index e7955c7..7e3c0f9 100644 --- a/pkg/views/src/layouts/master.vue +++ b/pkg/views/src/layouts/master.vue @@ -15,8 +15,8 @@ - - + +
diff --git a/pkg/views/src/router/index.ts b/pkg/views/src/router/index.ts index d78ea4a..b476ef8 100644 --- a/pkg/views/src/router/index.ts +++ b/pkg/views/src/router/index.ts @@ -15,7 +15,7 @@ const router = createRouter({ path: "/auth", children: [ { path: "sign-in", name: "auth.sign-in", component: () => import("@/views/auth/sign-in.vue") }, - // { path: "sign-up", name: "auth.sign-up", component: () => import("@/views/auth/sign-up.vue") }, + { path: "sign-up", name: "auth.sign-up", component: () => import("@/views/auth/sign-up.vue") }, ] } ], diff --git a/pkg/views/src/views/auth/sign-in.vue b/pkg/views/src/views/auth/sign-in.vue index 81b30f0..0308d03 100644 --- a/pkg/views/src/views/auth/sign-in.vue +++ b/pkg/views/src/views/auth/sign-in.vue @@ -1,5 +1,7 @@ - + + + +
From f0e24f634b40be11af824d93fcd387b9453c8830 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 13 Mar 2024 23:33:29 +0800 Subject: [PATCH 06/13] :sparkles: Auth guard --- pkg/views/src/router/index.ts | 36 +++++++++++++++++++++------ pkg/views/src/stores/userinfo.ts | 42 +++++++++++++++----------------- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/pkg/views/src/router/index.ts b/pkg/views/src/router/index.ts index b476ef8..aff742b 100644 --- a/pkg/views/src/router/index.ts +++ b/pkg/views/src/router/index.ts @@ -1,4 +1,5 @@ import { createRouter, createWebHistory } from "vue-router" +import { useUserinfo } from "@/stores/userinfo" import MasterLayout from "@/layouts/master.vue" const router = createRouter({ @@ -7,18 +8,39 @@ const router = createRouter({ { path: "/", component: MasterLayout, - children: [ - { path: "/", name: "dashboard", component: () => import("@/views/dashboard.vue") }, - ], + children: [{ path: "/", name: "dashboard", component: () => import("@/views/dashboard.vue") }], }, { path: "/auth", children: [ - { path: "sign-in", name: "auth.sign-in", component: () => import("@/views/auth/sign-in.vue") }, - { path: "sign-up", name: "auth.sign-up", component: () => import("@/views/auth/sign-up.vue") }, - ] - } + { + path: "sign-in", + name: "auth.sign-in", + component: () => import("@/views/auth/sign-in.vue"), + meta: { public: true }, + }, + { + path: "sign-up", + name: "auth.sign-up", + component: () => import("@/views/auth/sign-up.vue"), + meta: { public: true }, + }, + ], + }, ], }) +router.beforeEach(async (to, from, next) => { + const id = useUserinfo() + if (!id.isReady) { + await id.readProfiles() + } + + if (!to.meta.public && !id.userinfo.isLoggedIn) { + next({ name: "auth.sign-in", query: { redirect_uri: to.fullPath } }) + } else { + next() + } +}) + export default router diff --git a/pkg/views/src/stores/userinfo.ts b/pkg/views/src/stores/userinfo.ts index 2f4f1a8..8636dc7 100644 --- a/pkg/views/src/stores/userinfo.ts +++ b/pkg/views/src/stores/userinfo.ts @@ -4,17 +4,15 @@ 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 + data: null, } export function getAtk(): string { @@ -31,25 +29,25 @@ export const useUserinfo = defineStore("userinfo", () => { 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 - }; + isReady.value = true + } + + const res = await request("/api/users/me", { + headers: { Authorization: `Bearer ${getAtk()}` }, + }) + + if (res.status !== 200) { + return + } + + const data = await res.json() + + isReady.value = true + userinfo.value = { + isLoggedIn: true, + displayName: data["nick"], + data: data, + } } return { userinfo, isReady, readProfiles } From 6b32f4775846a1bdc343eb48bc3a49d16f2cd28e Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 16 Mar 2024 00:51:34 +0800 Subject: [PATCH 07/13] :sparkles: User personalize --- pkg/views/.eslintrc.js | 6 + pkg/views/src/layouts/master.vue | 2 +- pkg/views/src/layouts/user-center.vue | 18 +++ pkg/views/src/router/index.ts | 12 +- pkg/views/src/views/dashboard.vue | 30 ++++- pkg/views/src/views/personalize.vue | 185 ++++++++++++++++++++++++++ 6 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 pkg/views/.eslintrc.js create mode 100644 pkg/views/src/layouts/user-center.vue create mode 100644 pkg/views/src/views/personalize.vue diff --git a/pkg/views/.eslintrc.js b/pkg/views/.eslintrc.js new file mode 100644 index 0000000..b2d47e8 --- /dev/null +++ b/pkg/views/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ["plugin:vue/vue3-recommended"], + rules: { + "vue/multi-word-component-names": "off", + }, +} diff --git a/pkg/views/src/layouts/master.vue b/pkg/views/src/layouts/master.vue index 7553a77..e8b7d05 100644 --- a/pkg/views/src/layouts/master.vue +++ b/pkg/views/src/layouts/master.vue @@ -19,7 +19,7 @@ - + diff --git a/pkg/views/src/layouts/user-center.vue b/pkg/views/src/layouts/user-center.vue new file mode 100644 index 0000000..1919bc0 --- /dev/null +++ b/pkg/views/src/layouts/user-center.vue @@ -0,0 +1,18 @@ + diff --git a/pkg/views/src/router/index.ts b/pkg/views/src/router/index.ts index aff742b..7871bd6 100644 --- a/pkg/views/src/router/index.ts +++ b/pkg/views/src/router/index.ts @@ -1,6 +1,7 @@ import { createRouter, createWebHistory } from "vue-router" import { useUserinfo } from "@/stores/userinfo" import MasterLayout from "@/layouts/master.vue" +import UserCenterLayout from "@/layouts/user-center.vue" const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -8,7 +9,16 @@ const router = createRouter({ { path: "/", component: MasterLayout, - children: [{ path: "/", name: "dashboard", component: () => import("@/views/dashboard.vue") }], + children: [ + { + path: "/", + component: UserCenterLayout, + children: [ + { path: "/", name: "dashboard", component: () => import("@/views/dashboard.vue") }, + { path: "/me/personalize", name: "personalize", component: () => import("@/views/personalize.vue") }, + ], + }, + ], }, { path: "/auth", diff --git a/pkg/views/src/views/dashboard.vue b/pkg/views/src/views/dashboard.vue index 608217b..b52e447 100644 --- a/pkg/views/src/views/dashboard.vue +++ b/pkg/views/src/views/dashboard.vue @@ -1,3 +1,31 @@ + + + + diff --git a/pkg/views/src/views/personalize.vue b/pkg/views/src/views/personalize.vue new file mode 100644 index 0000000..f46d427 --- /dev/null +++ b/pkg/views/src/views/personalize.vue @@ -0,0 +1,185 @@ + + + + + From d28a79fdd98de59e0e465a7eee4daf1c281ba37f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 16 Mar 2024 01:44:42 +0800 Subject: [PATCH 08/13] :sparkles: User security center --- pkg/views/src/layouts/master.vue | 2 +- pkg/views/src/layouts/user-center.vue | 1 + pkg/views/src/router/index.ts | 1 + pkg/views/src/views/security.vue | 266 ++++++++++++++++++++++++++ 4 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 pkg/views/src/views/security.vue diff --git a/pkg/views/src/layouts/master.vue b/pkg/views/src/layouts/master.vue index e8b7d05..5dd8f16 100644 --- a/pkg/views/src/layouts/master.vue +++ b/pkg/views/src/layouts/master.vue @@ -10,7 +10,7 @@ diff --git a/pkg/views/src/layouts/user-center.vue b/pkg/views/src/layouts/user-center.vue index 1919bc0..437106f 100644 --- a/pkg/views/src/layouts/user-center.vue +++ b/pkg/views/src/layouts/user-center.vue @@ -6,6 +6,7 @@ + diff --git a/pkg/views/src/router/index.ts b/pkg/views/src/router/index.ts index 7871bd6..3979638 100644 --- a/pkg/views/src/router/index.ts +++ b/pkg/views/src/router/index.ts @@ -16,6 +16,7 @@ const router = createRouter({ children: [ { path: "/", name: "dashboard", component: () => import("@/views/dashboard.vue") }, { path: "/me/personalize", name: "personalize", component: () => import("@/views/personalize.vue") }, + { path: "/me/security", name: "security", component: () => import("@/views/security.vue") }, ], }, ], diff --git a/pkg/views/src/views/security.vue b/pkg/views/src/views/security.vue new file mode 100644 index 0000000..c081a9c --- /dev/null +++ b/pkg/views/src/views/security.vue @@ -0,0 +1,266 @@ + + + + + From fa59f87d3c1824885ded91a2f705bbf30ae24f9f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 16 Mar 2024 12:28:50 +0800 Subject: [PATCH 09/13] :sparkles: Notification list --- pkg/server/accounts_api.go | 8 +- pkg/server/notifications_api.go | 5 +- pkg/views/src/components/NotificationList.vue | 87 +++++++++++++++++++ pkg/views/src/components/UserMenu.vue | 43 +++++++++ pkg/views/src/layouts/master.vue | 38 ++------ pkg/views/src/views/security.vue | 2 +- 6 files changed, 146 insertions(+), 37 deletions(-) create mode 100644 pkg/views/src/components/NotificationList.vue create mode 100644 pkg/views/src/components/UserMenu.vue diff --git a/pkg/server/accounts_api.go b/pkg/server/accounts_api.go index 30947ca..738f296 100644 --- a/pkg/server/accounts_api.go +++ b/pkg/server/accounts_api.go @@ -1,15 +1,16 @@ package server import ( + "fmt" + "strconv" + "time" + "code.smartsheep.studio/hydrogen/identity/pkg/database" "code.smartsheep.studio/hydrogen/identity/pkg/models" "code.smartsheep.studio/hydrogen/identity/pkg/services" - "fmt" "github.com/gofiber/fiber/v2" jsoniter "github.com/json-iterator/go" "github.com/spf13/viper" - "strconv" - "time" ) func getUserinfo(c *fiber.Ctx) error { @@ -20,7 +21,6 @@ func getUserinfo(c *fiber.Ctx) error { Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}). Preload("Profile"). Preload("Contacts"). - Preload("Notifications", "read_at IS NULL"). First(&data).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } diff --git a/pkg/server/notifications_api.go b/pkg/server/notifications_api.go index c8f9cdf..1e84b38 100644 --- a/pkg/server/notifications_api.go +++ b/pkg/server/notifications_api.go @@ -1,12 +1,13 @@ package server import ( + "time" + "code.smartsheep.studio/hydrogen/identity/pkg/database" "code.smartsheep.studio/hydrogen/identity/pkg/models" "code.smartsheep.studio/hydrogen/identity/pkg/services" "github.com/gofiber/fiber/v2" "github.com/samber/lo" - "time" ) func getNotifications(c *fiber.Ctx) error { @@ -14,7 +15,7 @@ func getNotifications(c *fiber.Ctx) error { take := c.QueryInt("take", 0) offset := c.QueryInt("offset", 0) - only_unread := c.QueryBool("only_unread", true) + only_unread := !c.QueryBool("past", false) tx := database.C.Where(&models.Notification{RecipientID: user.ID}).Model(&models.Notification{}) if only_unread { diff --git a/pkg/views/src/components/NotificationList.vue b/pkg/views/src/components/NotificationList.vue new file mode 100644 index 0000000..c5cf0c5 --- /dev/null +++ b/pkg/views/src/components/NotificationList.vue @@ -0,0 +1,87 @@ + + + diff --git a/pkg/views/src/components/UserMenu.vue b/pkg/views/src/components/UserMenu.vue new file mode 100644 index 0000000..10a1c28 --- /dev/null +++ b/pkg/views/src/components/UserMenu.vue @@ -0,0 +1,43 @@ + + + diff --git a/pkg/views/src/layouts/master.vue b/pkg/views/src/layouts/master.vue index 5dd8f16..2231ccd 100644 --- a/pkg/views/src/layouts/master.vue +++ b/pkg/views/src/layouts/master.vue @@ -7,21 +7,13 @@ - - +
+ +
- - - - - - - -
+
+ +
@@ -31,26 +23,12 @@ diff --git a/pkg/views/src/views/security.vue b/pkg/views/src/views/security.vue index c081a9c..979527f 100644 --- a/pkg/views/src/views/security.vue +++ b/pkg/views/src/views/security.vue @@ -105,7 +105,7 @@ From 37a68eac282ff4fa486e91d1ac428f26b23009a6 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 16 Mar 2024 13:19:11 +0800 Subject: [PATCH 10/13] :sparkles: OAuth Connect --- pkg/views/src/router/index.ts | 5 + pkg/views/src/views/auth/claims.ts | 13 ++ pkg/views/src/views/auth/connect.vue | 191 +++++++++++++++++++++++++++ pkg/views/vite.config.ts | 3 +- 4 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 pkg/views/src/views/auth/claims.ts create mode 100644 pkg/views/src/views/auth/connect.vue diff --git a/pkg/views/src/router/index.ts b/pkg/views/src/router/index.ts index 3979638..8ce26e2 100644 --- a/pkg/views/src/router/index.ts +++ b/pkg/views/src/router/index.ts @@ -36,6 +36,11 @@ const router = createRouter({ component: () => import("@/views/auth/sign-up.vue"), meta: { public: true }, }, + { + path: "o/connect", + name: "openid.connect", + component: () => import("@/views/auth/connect.vue"), + }, ], }, ], diff --git a/pkg/views/src/views/auth/claims.ts b/pkg/views/src/views/auth/claims.ts new file mode 100644 index 0000000..6ca79e5 --- /dev/null +++ b/pkg/views/src/views/auth/claims.ts @@ -0,0 +1,13 @@ +export interface ClaimType { + icon: string + name: string + description: string +} + +export const claims: { [id: string]: ClaimType } = { + openid: { + icon: "mdi-identifier", + name: "Open Identity", + description: "Allow them to read your personal information.", + }, +} diff --git a/pkg/views/src/views/auth/connect.vue b/pkg/views/src/views/auth/connect.vue new file mode 100644 index 0000000..150090f --- /dev/null +++ b/pkg/views/src/views/auth/connect.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/pkg/views/vite.config.ts b/pkg/views/vite.config.ts index 9208dfc..3329c64 100644 --- a/pkg/views/vite.config.ts +++ b/pkg/views/vite.config.ts @@ -15,7 +15,8 @@ export default defineConfig({ }, server: { proxy: { - "/api": "http://localhost:8444" + "/api": "http://localhost:8444", + "/.well-known": "http://localhost:8444" } } }) From f3473aeb8394d2b4a8ac5a12d359daebdfc3c39a Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 16 Mar 2024 13:25:39 +0800 Subject: [PATCH 11/13] :lipstick: Better title --- pkg/views/src/router/index.ts | 31 +++++++++++++++++++++++----- pkg/views/src/views/auth/connect.vue | 1 + 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/pkg/views/src/router/index.ts b/pkg/views/src/router/index.ts index 8ce26e2..c0981ee 100644 --- a/pkg/views/src/router/index.ts +++ b/pkg/views/src/router/index.ts @@ -14,9 +14,24 @@ const router = createRouter({ path: "/", component: UserCenterLayout, children: [ - { path: "/", name: "dashboard", component: () => import("@/views/dashboard.vue") }, - { path: "/me/personalize", name: "personalize", component: () => import("@/views/personalize.vue") }, - { path: "/me/security", name: "security", component: () => import("@/views/security.vue") }, + { + path: "/", + name: "dashboard", + component: () => import("@/views/dashboard.vue"), + meta: { title: "Your account" }, + }, + { + path: "/me/personalize", + name: "personalize", + component: () => import("@/views/personalize.vue"), + meta: { title: "Your personality" }, + }, + { + path: "/me/security", + name: "security", + component: () => import("@/views/security.vue"), + meta: { title: "Your security" }, + }, ], }, ], @@ -28,13 +43,13 @@ const router = createRouter({ path: "sign-in", name: "auth.sign-in", component: () => import("@/views/auth/sign-in.vue"), - meta: { public: true }, + meta: { public: true, title: "Sign in" }, }, { path: "sign-up", name: "auth.sign-up", component: () => import("@/views/auth/sign-up.vue"), - meta: { public: true }, + meta: { public: true, title: "Sign up" }, }, { path: "o/connect", @@ -52,6 +67,12 @@ router.beforeEach(async (to, from, next) => { await id.readProfiles() } + if (to.meta.title) { + document.title = `Solarpass | ${to.meta.title}` + } else { + document.title = "Solarpass" + } + if (!to.meta.public && !id.userinfo.isLoggedIn) { next({ name: "auth.sign-in", query: { redirect_uri: to.fullPath } }) } else { diff --git a/pkg/views/src/views/auth/connect.vue b/pkg/views/src/views/auth/connect.vue index 150090f..ea62fb0 100644 --- a/pkg/views/src/views/auth/connect.vue +++ b/pkg/views/src/views/auth/connect.vue @@ -120,6 +120,7 @@ async function preconnect() { panel.value = "callback" callback(data["session"]) } else { + document.title = `Solarpass | Connect to ${data["client"]?.name}` metadata.value = data["client"] loading.value = false } From d59818e857c21fdc9a5186f7ff8ed73238e8bce7 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 16 Mar 2024 14:25:36 +0800 Subject: [PATCH 12/13] :bento: Icon --- pkg/views/index.html | 2 +- pkg/views/public/favicon.svg | 20 ++++++++++++++++++++ pkg/views/src/layouts/master.vue | 7 ++++++- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100755 pkg/views/public/favicon.svg diff --git a/pkg/views/index.html b/pkg/views/index.html index 0e55e96..15de511 100644 --- a/pkg/views/index.html +++ b/pkg/views/index.html @@ -2,7 +2,7 @@ - + Solarpass diff --git a/pkg/views/public/favicon.svg b/pkg/views/public/favicon.svg new file mode 100755 index 0000000..8adaf6a --- /dev/null +++ b/pkg/views/public/favicon.svg @@ -0,0 +1,20 @@ + + + + + + diff --git a/pkg/views/src/layouts/master.vue b/pkg/views/src/layouts/master.vue index 2231ccd..ebbe1a8 100644 --- a/pkg/views/src/layouts/master.vue +++ b/pkg/views/src/layouts/master.vue @@ -1,7 +1,8 @@