✨ 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", | ||||
|     "@mui/icons-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", | ||||
|     "react": "^19.0.0", | ||||
|     "react-dom": "^19.0.0" | ||||
|     "react-dom": "^19.0.0", | ||||
|     "react-hook-form": "^7.54.2" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "typescript": "^5", | ||||
|   | ||||
| @@ -1,35 +1,35 @@ | ||||
| import { AppBar, Box, IconButton, Toolbar, Typography } from "@mui/material"; | ||||
| import MenuIcon from "@mui/icons-material/Menu"; | ||||
| import AccountCircle from "@mui/icons-material/AccountCircle"; | ||||
| import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material' | ||||
| import MenuIcon from '@mui/icons-material/Menu' | ||||
| import AccountCircle from '@mui/icons-material/AccountCircle' | ||||
| import Link from 'next/link' | ||||
|  | ||||
| export function CapAppBar() { | ||||
|   return ( | ||||
|     <Box sx={{ flexGrow: 1 }}> | ||||
|       <AppBar position="static" elevation={0} color="transparent"> | ||||
|         <Toolbar> | ||||
|           <IconButton | ||||
|             size="large" | ||||
|             edge="start" | ||||
|             color="inherit" | ||||
|             aria-label="menu" | ||||
|             sx={{ mr: 2 }} | ||||
|           > | ||||
|           <IconButton size="large" edge="start" color="inherit" aria-label="menu" sx={{ mr: 2 }}> | ||||
|             <MenuIcon /> | ||||
|           </IconButton> | ||||
|           <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> | ||||
|             Capital | ||||
|           </Typography> | ||||
|           <IconButton | ||||
|             size="large" | ||||
|             aria-label="account of current user" | ||||
|             aria-controls="primary-search-account-menu" | ||||
|             aria-haspopup="true" | ||||
|             color="inherit" | ||||
|           > | ||||
|             <AccountCircle /> | ||||
|           </IconButton> | ||||
|           <Link href="/" passHref style={{ flexGrow: 1 }}> | ||||
|             <Typography variant="h6" component="div"> | ||||
|               Capital | ||||
|             </Typography> | ||||
|           </Link> | ||||
|  | ||||
|           <Link href="/auth/login" passHref> | ||||
|             <IconButton | ||||
|               size="large" | ||||
|               aria-label="account of current user" | ||||
|               aria-controls="primary-search-account-menu" | ||||
|               aria-haspopup="true" | ||||
|               color="inherit" | ||||
|             > | ||||
|               <AccountCircle /> | ||||
|             </IconButton> | ||||
|           </Link> | ||||
|         </Toolbar> | ||||
|       </AppBar> | ||||
|     </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 type { AppProps } from "next/app"; | ||||
| import { createTheme, CssBaseline, ThemeProvider } from "@mui/material"; | ||||
| import { Roboto } from "next/font/google"; | ||||
| import { CapAppBar } from "@/components/CapAppBar"; | ||||
| import '@/styles/globals.css' | ||||
| import type { AppProps } from 'next/app' | ||||
| import { Box, createTheme, CssBaseline, ThemeProvider } from '@mui/material' | ||||
| import { Roboto } from 'next/font/google' | ||||
| import { CapAppBar } from '@/components/CapAppBar' | ||||
|  | ||||
| const fontRoboto = Roboto({ | ||||
|   subsets: ["latin"], | ||||
|   weight: ["400", "500", "700"], | ||||
|   display: "swap", | ||||
| }); | ||||
|   subsets: ['latin'], | ||||
|   weight: ['400', '500', '700'], | ||||
|   display: 'swap', | ||||
| }) | ||||
|  | ||||
| const siteTheme = createTheme({ | ||||
|   palette: { | ||||
|     mode: "light", | ||||
|     mode: 'light', | ||||
|     primary: { | ||||
|       main: "#3949ab", | ||||
|       main: '#3949ab', | ||||
|     }, | ||||
|     secondary: { | ||||
|       main: "#1e88e5", | ||||
|       main: '#1e88e5', | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| }) | ||||
|  | ||||
| export default function App({ Component, pageProps }: AppProps) { | ||||
|   return ( | ||||
| @@ -35,8 +35,10 @@ export default function App({ Component, pageProps }: AppProps) { | ||||
|         <CssBaseline /> | ||||
|  | ||||
|         <CapAppBar /> | ||||
|         <Component {...pageProps} /> | ||||
|         <Box sx={{ height: 'calc(100vh - 64px)' }}> | ||||
|           <Component {...pageProps} /> | ||||
|         </Box> | ||||
|       </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() { | ||||
|   return ( | ||||
| @@ -9,5 +9,5 @@ export default function Document() { | ||||
|         <NextScript /> | ||||
|       </body> | ||||
|     </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() { | ||||
|   return ( | ||||
| @@ -10,5 +10,5 @@ export default function Home() { | ||||
|         </Typography> | ||||
|       </Container> | ||||
|     </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