✨ User login
This commit is contained in:
		
							
								
								
									
										7
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | { | ||||||
|  |   "semi": false, | ||||||
|  |   "printWidth": 120, | ||||||
|  |   "tabWidth": 2, | ||||||
|  |   "trailingComma": "all", | ||||||
|  |   "singleQuote": true | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,40 +0,0 @@ | |||||||
| This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app). |  | ||||||
|  |  | ||||||
| ## Getting Started |  | ||||||
|  |  | ||||||
| First, run the development server: |  | ||||||
|  |  | ||||||
| ```bash |  | ||||||
| npm run dev |  | ||||||
| # or |  | ||||||
| yarn dev |  | ||||||
| # or |  | ||||||
| pnpm dev |  | ||||||
| # or |  | ||||||
| bun dev |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. |  | ||||||
|  |  | ||||||
| You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. |  | ||||||
|  |  | ||||||
| [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. |  | ||||||
|  |  | ||||||
| The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages. |  | ||||||
|  |  | ||||||
| This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. |  | ||||||
|  |  | ||||||
| ## Learn More |  | ||||||
|  |  | ||||||
| To learn more about Next.js, take a look at the following resources: |  | ||||||
|  |  | ||||||
| - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. |  | ||||||
| - [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial. |  | ||||||
|  |  | ||||||
| You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! |  | ||||||
|  |  | ||||||
| ## Deploy on Vercel |  | ||||||
|  |  | ||||||
| The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. |  | ||||||
|  |  | ||||||
| Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details. |  | ||||||
| @@ -13,9 +13,13 @@ | |||||||
|     "@emotion/styled": "^11.14.0", |     "@emotion/styled": "^11.14.0", | ||||||
|     "@mui/icons-material": "^6.3.0", |     "@mui/icons-material": "^6.3.0", | ||||||
|     "@mui/material": "^6.3.0", |     "@mui/material": "^6.3.0", | ||||||
|  |     "axios": "^1.7.9", | ||||||
|  |     "axios-case-converter": "^1.1.1", | ||||||
|  |     "cookies-next": "^5.0.2", | ||||||
|     "next": "15.1.3", |     "next": "15.1.3", | ||||||
|     "react": "^19.0.0", |     "react": "^19.0.0", | ||||||
|     "react-dom": "^19.0.0" |     "react-dom": "^19.0.0", | ||||||
|  |     "react-hook-form": "^7.54.2" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "typescript": "^5", |     "typescript": "^5", | ||||||
|   | |||||||
| @@ -1,24 +1,23 @@ | |||||||
| import { AppBar, Box, IconButton, Toolbar, Typography } from "@mui/material"; | import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material' | ||||||
| import MenuIcon from "@mui/icons-material/Menu"; | import MenuIcon from '@mui/icons-material/Menu' | ||||||
| import AccountCircle from "@mui/icons-material/AccountCircle"; | import AccountCircle from '@mui/icons-material/AccountCircle' | ||||||
|  | import Link from 'next/link' | ||||||
|  |  | ||||||
| export function CapAppBar() { | export function CapAppBar() { | ||||||
|   return ( |   return ( | ||||||
|     <Box sx={{ flexGrow: 1 }}> |     <Box sx={{ flexGrow: 1 }}> | ||||||
|       <AppBar position="static" elevation={0} color="transparent"> |       <AppBar position="static" elevation={0} color="transparent"> | ||||||
|         <Toolbar> |         <Toolbar> | ||||||
|           <IconButton |           <IconButton size="large" edge="start" color="inherit" aria-label="menu" sx={{ mr: 2 }}> | ||||||
|             size="large" |  | ||||||
|             edge="start" |  | ||||||
|             color="inherit" |  | ||||||
|             aria-label="menu" |  | ||||||
|             sx={{ mr: 2 }} |  | ||||||
|           > |  | ||||||
|             <MenuIcon /> |             <MenuIcon /> | ||||||
|           </IconButton> |           </IconButton> | ||||||
|           <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> |           <Link href="/" passHref style={{ flexGrow: 1 }}> | ||||||
|  |             <Typography variant="h6" component="div"> | ||||||
|               Capital |               Capital | ||||||
|             </Typography> |             </Typography> | ||||||
|  |           </Link> | ||||||
|  |  | ||||||
|  |           <Link href="/auth/login" passHref> | ||||||
|             <IconButton |             <IconButton | ||||||
|               size="large" |               size="large" | ||||||
|               aria-label="account of current user" |               aria-label="account of current user" | ||||||
| @@ -28,8 +27,9 @@ export function CapAppBar() { | |||||||
|             > |             > | ||||||
|               <AccountCircle /> |               <AccountCircle /> | ||||||
|             </IconButton> |             </IconButton> | ||||||
|  |           </Link> | ||||||
|         </Toolbar> |         </Toolbar> | ||||||
|       </AppBar> |       </AppBar> | ||||||
|     </Box> |     </Box> | ||||||
|   ); |   ) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										83
									
								
								src/components/auth/SnLoginCheckpoint.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/components/auth/SnLoginCheckpoint.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | |||||||
|  | 'use client' | ||||||
|  |  | ||||||
|  | import { SnAuthFactor, SnAuthResult, SnAuthTicket } from '@/services/auth' | ||||||
|  | import { sni } from '@/services/network' | ||||||
|  | import { ArrowForward } from '@mui/icons-material' | ||||||
|  | import { Collapse, Alert, Box, TextField, Button } from '@mui/material' | ||||||
|  | import { useState } from 'react' | ||||||
|  | import { useForm } from 'react-hook-form' | ||||||
|  |  | ||||||
|  | import ErrorIcon from '@mui/icons-material/Error' | ||||||
|  | import { setCookie } from 'cookies-next/client' | ||||||
|  |  | ||||||
|  | export interface SnLoginCheckpointForm { | ||||||
|  |   password: string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function SnLoginCheckpoint({ | ||||||
|  |   ticket, | ||||||
|  |   factor, | ||||||
|  |   onNext, | ||||||
|  | }: { | ||||||
|  |   ticket: SnAuthTicket | ||||||
|  |   factor: SnAuthFactor | ||||||
|  |   onNext: (val: SnAuthTicket, done: boolean) => void | ||||||
|  | }) { | ||||||
|  |   const [error, setError] = useState<string | null>(null) | ||||||
|  |   const [loading, setLoading] = useState<boolean>(false) | ||||||
|  |  | ||||||
|  |   const { handleSubmit, register } = useForm<SnLoginCheckpointForm>() | ||||||
|  |  | ||||||
|  |   async function onSubmit(data: any) { | ||||||
|  |     try { | ||||||
|  |       setLoading(true) | ||||||
|  |       const resp = await sni.patch<SnAuthResult>('/cgi/id/auth', { | ||||||
|  |         ticket_id: ticket.id, | ||||||
|  |         factor_id: factor.id, | ||||||
|  |         code: data.password, | ||||||
|  |       }) | ||||||
|  |  | ||||||
|  |       if (resp.data.isFinished) { | ||||||
|  |         const tokenResp = await sni.post('/cgi/id/auth/token', { | ||||||
|  |           grant_type: 'grant_token', | ||||||
|  |           code: resp.data.ticket.grantToken!, | ||||||
|  |         }) | ||||||
|  |         const atk: string = tokenResp.data['accessToken'] | ||||||
|  |         const rtk: string = tokenResp.data['refreshToken'] | ||||||
|  |         setCookie('nex_user_atk', atk, { path: '/', maxAge: 2592000 }) | ||||||
|  |         setCookie('nex_user_rtk', rtk, { path: '/', maxAge: 2592000 }) | ||||||
|  |         console.log('[Authenticator] User has been logged in. Result atk: ', atk) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       onNext(resp.data.ticket, resp.data.isFinished) | ||||||
|  |     } catch (err: any) { | ||||||
|  |       setError(err.toString()) | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Collapse in={!!error} sx={{ width: 320 }}> | ||||||
|  |         <Alert sx={{ mb: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error"> | ||||||
|  |           {error} | ||||||
|  |         </Alert> | ||||||
|  |       </Collapse> | ||||||
|  |  | ||||||
|  |       <form onSubmit={handleSubmit(onSubmit)}> | ||||||
|  |         <Box sx={{ display: 'flex', flexDirection: 'column', width: 320, gap: 2, textAlign: 'center' }}> | ||||||
|  |           <TextField | ||||||
|  |             label={factor.type == 0 ? 'Password' : 'Verification code'} | ||||||
|  |             type="password" | ||||||
|  |             {...register('password', { required: true })} | ||||||
|  |           /> | ||||||
|  |  | ||||||
|  |           <Button variant="contained" endIcon={<ArrowForward />} disabled={loading} type="submit"> | ||||||
|  |             Next | ||||||
|  |           </Button> | ||||||
|  |         </Box> | ||||||
|  |       </form> | ||||||
|  |     </> | ||||||
|  |   ) | ||||||
|  | } | ||||||
							
								
								
									
										68
									
								
								src/components/auth/SnLoginRouter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/components/auth/SnLoginRouter.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | |||||||
|  | 'use client' | ||||||
|  |  | ||||||
|  | import { SnAuthFactor, SnAuthTicket } from '@/services/auth' | ||||||
|  | import { sni } from '@/services/network' | ||||||
|  | import { Collapse, Alert, Box, Button, Typography, ButtonGroup } from '@mui/material' | ||||||
|  | import { useState } from 'react' | ||||||
|  |  | ||||||
|  | import ErrorIcon from '@mui/icons-material/Error' | ||||||
|  | import PasswordIcon from '@mui/icons-material/Password' | ||||||
|  | import EmailIcon from '@mui/icons-material/Email' | ||||||
|  |  | ||||||
|  | export function SnLoginRouter({ | ||||||
|  |   ticket, | ||||||
|  |   factorList, | ||||||
|  |   onNext, | ||||||
|  | }: { | ||||||
|  |   ticket: SnAuthTicket | ||||||
|  |   factorList: SnAuthFactor[] | ||||||
|  |   onNext: (val: SnAuthFactor) => void | ||||||
|  | }) { | ||||||
|  |   const factorTypeIcons = [<PasswordIcon />, <EmailIcon />] | ||||||
|  |   const factorTypeLabels = ['Password', 'Email verification code'] | ||||||
|  |  | ||||||
|  |   const [error, setError] = useState<string | null>(null) | ||||||
|  |   const [loading, setLoading] = useState<boolean>(false) | ||||||
|  |  | ||||||
|  |   async function onSubmit(factor: SnAuthFactor) { | ||||||
|  |     try { | ||||||
|  |       setLoading(true) | ||||||
|  |       await sni.post('/cgi/id/auth/factors/' + factor.id) | ||||||
|  |       onNext(factor) | ||||||
|  |     } catch (err: any) { | ||||||
|  |       setError(err.toString()) | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Collapse in={!!error} sx={{ width: 320 }}> | ||||||
|  |         <Alert sx={{ mb: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error"> | ||||||
|  |           {error} | ||||||
|  |         </Alert> | ||||||
|  |       </Collapse> | ||||||
|  |  | ||||||
|  |       <Box sx={{ display: 'flex', flexDirection: 'column', width: 320, gap: 2, textAlign: 'center' }}> | ||||||
|  |         <ButtonGroup orientation="vertical" aria-label="Vertical button group"> | ||||||
|  |           {factorList.map((factor) => ( | ||||||
|  |             <Button | ||||||
|  |               sx={{ py: 1 }} | ||||||
|  |               key={factor.id} | ||||||
|  |               onClick={() => onSubmit(factor)} | ||||||
|  |               disabled={loading || ticket.factorTrail?.includes(factor.id)} | ||||||
|  |               startIcon={factorTypeIcons[factor.type]} | ||||||
|  |             > | ||||||
|  |               {factorTypeLabels[factor.type]} | ||||||
|  |             </Button> | ||||||
|  |           ))} | ||||||
|  |         </ButtonGroup> | ||||||
|  |  | ||||||
|  |         <Typography variant="caption" sx={{ opacity: 0.75, mx: 2 }}> | ||||||
|  |           {ticket.stepRemain} step(s) left | ||||||
|  |         </Typography> | ||||||
|  |       </Box> | ||||||
|  |     </> | ||||||
|  |   ) | ||||||
|  | } | ||||||
							
								
								
									
										67
									
								
								src/components/auth/SnLoginStart.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/components/auth/SnLoginStart.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | 'use client' | ||||||
|  |  | ||||||
|  | import { useState } from 'react' | ||||||
|  | import { sni } from '@/services/network' | ||||||
|  | import { ArrowForward } from '@mui/icons-material' | ||||||
|  | import { Alert, Box, Button, Collapse, Link, TextField, Typography } from '@mui/material' | ||||||
|  | import { SnAuthFactor, SnAuthResult, SnAuthTicket } from '@/services/auth' | ||||||
|  | import { useForm } from 'react-hook-form' | ||||||
|  |  | ||||||
|  | import ErrorIcon from '@mui/icons-material/Error' | ||||||
|  |  | ||||||
|  | export type SnLoginStartForm = { | ||||||
|  |   username: string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function SnLoginStart({ onNext }: { onNext: (val: SnAuthTicket, fcs: SnAuthFactor[]) => void }) { | ||||||
|  |   const [error, setError] = useState<string | null>(null) | ||||||
|  |   const [loading, setLoading] = useState<boolean>(false) | ||||||
|  |  | ||||||
|  |   const { handleSubmit, register } = useForm<SnLoginStartForm>() | ||||||
|  |  | ||||||
|  |   async function onSubmit(data: any) { | ||||||
|  |     try { | ||||||
|  |       setLoading(true) | ||||||
|  |       const resp = await sni.post<SnAuthResult>('/cgi/id/auth', data) | ||||||
|  |       const factorResp = await sni.get<SnAuthFactor[]>('/cgi/id/auth/factors', { | ||||||
|  |         params: { | ||||||
|  |           ticketId: resp.data.ticket.id, | ||||||
|  |         }, | ||||||
|  |       }) | ||||||
|  |       onNext(resp.data.ticket, factorResp.data) | ||||||
|  |     } catch (err: any) { | ||||||
|  |       setError(err.toString()) | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Collapse in={!!error} sx={{ width: 320 }}> | ||||||
|  |         <Alert sx={{ mb: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error"> | ||||||
|  |           {error} | ||||||
|  |         </Alert> | ||||||
|  |       </Collapse> | ||||||
|  |  | ||||||
|  |       <form onSubmit={handleSubmit(onSubmit)}> | ||||||
|  |         <Box sx={{ display: 'flex', flexDirection: 'column', width: 320, gap: 2, textAlign: 'center' }}> | ||||||
|  |           <TextField | ||||||
|  |             label="Username" | ||||||
|  |             helperText="You can also use email address and phone number" | ||||||
|  |             {...register('username', { required: true })} | ||||||
|  |           /> | ||||||
|  |  | ||||||
|  |           <Button variant="contained" endIcon={<ArrowForward />} disabled={loading} type="submit"> | ||||||
|  |             Next | ||||||
|  |           </Button> | ||||||
|  |  | ||||||
|  |           <Typography variant="caption" sx={{ opacity: 0.75, mx: 2 }}> | ||||||
|  |             By continuing means you agree to our <Link href="#">Terms of Service</Link> and{' '} | ||||||
|  |             <Link href="#">Privacy Policy</Link> | ||||||
|  |           </Typography> | ||||||
|  |         </Box> | ||||||
|  |       </form> | ||||||
|  |     </> | ||||||
|  |   ) | ||||||
|  | } | ||||||
| @@ -1,26 +1,26 @@ | |||||||
| import "@/styles/globals.css"; | import '@/styles/globals.css' | ||||||
| import type { AppProps } from "next/app"; | import type { AppProps } from 'next/app' | ||||||
| import { createTheme, CssBaseline, ThemeProvider } from "@mui/material"; | import { Box, createTheme, CssBaseline, ThemeProvider } from '@mui/material' | ||||||
| import { Roboto } from "next/font/google"; | import { Roboto } from 'next/font/google' | ||||||
| import { CapAppBar } from "@/components/CapAppBar"; | import { CapAppBar } from '@/components/CapAppBar' | ||||||
|  |  | ||||||
| const fontRoboto = Roboto({ | const fontRoboto = Roboto({ | ||||||
|   subsets: ["latin"], |   subsets: ['latin'], | ||||||
|   weight: ["400", "500", "700"], |   weight: ['400', '500', '700'], | ||||||
|   display: "swap", |   display: 'swap', | ||||||
| }); | }) | ||||||
|  |  | ||||||
| const siteTheme = createTheme({ | const siteTheme = createTheme({ | ||||||
|   palette: { |   palette: { | ||||||
|     mode: "light", |     mode: 'light', | ||||||
|     primary: { |     primary: { | ||||||
|       main: "#3949ab", |       main: '#3949ab', | ||||||
|     }, |     }, | ||||||
|     secondary: { |     secondary: { | ||||||
|       main: "#1e88e5", |       main: '#1e88e5', | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }); | }) | ||||||
|  |  | ||||||
| export default function App({ Component, pageProps }: AppProps) { | export default function App({ Component, pageProps }: AppProps) { | ||||||
|   return ( |   return ( | ||||||
| @@ -35,8 +35,10 @@ export default function App({ Component, pageProps }: AppProps) { | |||||||
|         <CssBaseline /> |         <CssBaseline /> | ||||||
|  |  | ||||||
|         <CapAppBar /> |         <CapAppBar /> | ||||||
|  |         <Box sx={{ height: 'calc(100vh - 64px)' }}> | ||||||
|           <Component {...pageProps} /> |           <Component {...pageProps} /> | ||||||
|  |         </Box> | ||||||
|       </ThemeProvider> |       </ThemeProvider> | ||||||
|     </> |     </> | ||||||
|   ); |   ) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { Html, Head, Main, NextScript } from "next/document"; | import { Html, Head, Main, NextScript } from 'next/document' | ||||||
|  |  | ||||||
| export default function Document() { | export default function Document() { | ||||||
|   return ( |   return ( | ||||||
| @@ -9,5 +9,5 @@ export default function Document() { | |||||||
|         <NextScript /> |         <NextScript /> | ||||||
|       </body> |       </body> | ||||||
|     </Html> |     </Html> | ||||||
|   ); |   ) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										79
									
								
								src/pages/auth/login.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/pages/auth/login.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | |||||||
|  | import { SnLoginCheckpoint } from '@/components/auth/SnLoginCheckpoint' | ||||||
|  | import { SnLoginRouter } from '@/components/auth/SnLoginRouter' | ||||||
|  | import { SnLoginStart } from '@/components/auth/SnLoginStart' | ||||||
|  | import { SnAuthFactor, SnAuthTicket } from '@/services/auth' | ||||||
|  | import { Box, Container, Typography } from '@mui/material' | ||||||
|  | import { useRouter } from 'next/router' | ||||||
|  | import { useState } from 'react' | ||||||
|  |  | ||||||
|  | export default function Login() { | ||||||
|  |   const [period, setPeriod] = useState<number>(0) | ||||||
|  |   const [ticket, setTicket] = useState<SnAuthTicket | null>(null) | ||||||
|  |   const [factorList, setFactorList] = useState<SnAuthFactor[]>([]) | ||||||
|  |   const [factor, setFactor] = useState<SnAuthFactor | null>(null) | ||||||
|  |  | ||||||
|  |   const router = useRouter() | ||||||
|  |  | ||||||
|  |   function renderForm() { | ||||||
|  |     switch (period) { | ||||||
|  |       case 1: | ||||||
|  |         return ( | ||||||
|  |           <SnLoginRouter | ||||||
|  |             ticket={ticket!} | ||||||
|  |             factorList={factorList} | ||||||
|  |             onNext={(val) => { | ||||||
|  |               setPeriod(period + 1) | ||||||
|  |               setFactor(val) | ||||||
|  |             }} | ||||||
|  |           /> | ||||||
|  |         ) | ||||||
|  |       case 2: | ||||||
|  |         return ( | ||||||
|  |           <SnLoginCheckpoint | ||||||
|  |             ticket={ticket!} | ||||||
|  |             factor={factor!} | ||||||
|  |             onNext={(val, done) => { | ||||||
|  |               if (!done) { | ||||||
|  |                 setTicket(val) | ||||||
|  |                 setPeriod(1) | ||||||
|  |                 return | ||||||
|  |               } | ||||||
|  |               router.push('/') | ||||||
|  |             }} | ||||||
|  |           /> | ||||||
|  |         ) | ||||||
|  |       default: | ||||||
|  |         return ( | ||||||
|  |           <SnLoginStart | ||||||
|  |             onNext={(val, fcs) => { | ||||||
|  |               setPeriod(period + 1) | ||||||
|  |               setTicket(val) | ||||||
|  |               setFactorList(fcs) | ||||||
|  |             }} | ||||||
|  |           /> | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Container | ||||||
|  |       sx={{ | ||||||
|  |         display: 'grid', | ||||||
|  |         placeItems: 'center', | ||||||
|  |         height: '100%', | ||||||
|  |         textAlign: 'center', | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       <Box> | ||||||
|  |         <Typography variant="h5" component="h1"> | ||||||
|  |           Login | ||||||
|  |         </Typography> | ||||||
|  |         <Typography variant="subtitle2" component="h2"> | ||||||
|  |           Login via Solarpass | ||||||
|  |         </Typography> | ||||||
|  |  | ||||||
|  |         <Box sx={{ mt: 3 }}>{renderForm()}</Box> | ||||||
|  |       </Box> | ||||||
|  |     </Container> | ||||||
|  |   ) | ||||||
|  | } | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { Container, Typography } from "@mui/material"; | import { Container, Typography } from '@mui/material' | ||||||
|  |  | ||||||
| export default function Home() { | export default function Home() { | ||||||
|   return ( |   return ( | ||||||
| @@ -10,5 +10,5 @@ export default function Home() { | |||||||
|         </Typography> |         </Typography> | ||||||
|       </Container> |       </Container> | ||||||
|     </main> |     </main> | ||||||
|   ); |   ) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								src/services/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/services/auth.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | export interface SnAuthResult { | ||||||
|  |   isFinished: boolean | ||||||
|  |   ticket: SnAuthTicket | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface SnAuthTicket { | ||||||
|  |   id: number | ||||||
|  |   createdAt: Date | ||||||
|  |   updatedAt: Date | ||||||
|  |   deletedAt?: Date | null | ||||||
|  |   stepRemain: number | ||||||
|  |   grantToken?: string | null | ||||||
|  |   accessToken?: string | null | ||||||
|  |   refreshToken?: string | null | ||||||
|  |   ipAddress: string | ||||||
|  |   location: string | ||||||
|  |   userAgent: string | ||||||
|  |   expiredAt?: Date | null | ||||||
|  |   lastGrantAt?: Date | null | ||||||
|  |   availableAt?: Date | null | ||||||
|  |   nonce?: string | null | ||||||
|  |   accountId?: number | null | ||||||
|  |   factorTrail: number[] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface SnAuthFactor { | ||||||
|  |   id: number | ||||||
|  |   createdAt: Date | ||||||
|  |   updatedAt: Date | ||||||
|  |   deletedAt?: Date | null | ||||||
|  |   type: number | ||||||
|  |   config?: Record<string, any> | null | ||||||
|  |   accountId?: number | null | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								src/services/network.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/services/network.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | import axios from 'axios' | ||||||
|  | import applyCaseMiddleware from 'axios-case-converter' | ||||||
|  |  | ||||||
|  | export let sni = applyCaseMiddleware( | ||||||
|  |   axios.create({ | ||||||
|  |     baseURL: 'https://api.sn.solsynth.dev', | ||||||
|  |   }), | ||||||
|  |   { | ||||||
|  |     ignoreParams: true, | ||||||
|  |     ignoreHeaders: true, | ||||||
|  |   }, | ||||||
|  | ) | ||||||
		Reference in New Issue
	
	Block a user