✨ User login
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user