feature: continue as @user
This commit is contained in:
parent
eff2103c3e
commit
dc9e0852ac
@ -1,8 +1,8 @@
|
||||
import { AppService } from './app.service';
|
||||
import { AuthParams, Params } from './decorators/auth-mode.decorator';
|
||||
import { AuthToken } from './decorators/token.decorator';
|
||||
import { Controller, Get, HttpStatus, Req, Res, UnauthorizedException } from '@nestjs/common';
|
||||
import { ApiExcludeController } from '@nestjs/swagger';
|
||||
import { Controller, Get, HttpStatus, Req, Res } from '@nestjs/common';
|
||||
import { ApiExcludeController, ApiResponse } from '@nestjs/swagger';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
@Controller()
|
||||
@ -33,7 +33,19 @@ export class AppController {
|
||||
|
||||
return reply.send();
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException(error);
|
||||
return reply.status(HttpStatus.UNAUTHORIZED).send({ message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/check-auth')
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
})
|
||||
public async checkAuth(
|
||||
@AuthParams() { authMode }: Params,
|
||||
@Req() req: FastifyRequest,
|
||||
@Res() reply: FastifyReply
|
||||
) {
|
||||
return reply.redirect(308, `${req.protocol}://${req.headers.host}/${authMode}/check-auth`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,4 +111,18 @@ export class LdapTfaController extends LdapController {
|
||||
|
||||
return reply.setCookie(env.COOKIE_TOKEN_NAME, activatedToken, cookieOptions).status(200).send();
|
||||
}
|
||||
|
||||
@Get('/check-auth')
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
})
|
||||
async checkAuth(@AuthToken() token: string, @Res() reply: FastifyReply) {
|
||||
const { authId } = await this.ldapTfaService.parseToken(token, { ignoreExpiration: true });
|
||||
|
||||
if (authId) return reply.status(HttpStatus.UNAUTHORIZED).send();
|
||||
|
||||
const user = await this.ldapTfaService.getUser(token, { ignoreExpiration: true });
|
||||
|
||||
return reply.status(200).send(user);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { LdapTfaGateway } from 'src/ldap-tfa/ldap-tfa.gateway';
|
||||
|
||||
@Module({
|
||||
controllers: [LdapTfaController],
|
||||
exports: [LdapTfaService],
|
||||
imports: [LdapModule],
|
||||
providers: [LdapTfaGateway, LdapTfaService],
|
||||
})
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
/* eslint-disable unicorn/no-object-as-default-parameter */
|
||||
import type { TokenPayload } from '../types/jwt';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Inject, UnauthorizedException } from '@nestjs/common';
|
||||
import type { JwtVerifyOptions } from '@nestjs/jwt';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { env } from 'src/config/env';
|
||||
@ -36,17 +38,17 @@ export class LdapTfaService extends LdapService {
|
||||
}
|
||||
}
|
||||
|
||||
public async parseToken(token: string) {
|
||||
public async parseToken(token: string, options: JwtVerifyOptions = { audience: 'auth' }) {
|
||||
try {
|
||||
return this.jwtService.verify<TokenPayload>(token, { audience: 'auth' });
|
||||
return this.jwtService.verify<TokenPayload>(token, options);
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async activateToken(token: string) {
|
||||
public async activateToken(token: string, options: JwtVerifyOptions = { audience: 'auth' }) {
|
||||
try {
|
||||
const { username } = this.jwtService.verify<TokenPayload>(token, { audience: 'auth' });
|
||||
const { username } = this.jwtService.verify<TokenPayload>(token, options);
|
||||
const user = await ldap.authenticate(username);
|
||||
await this.cacheManager.set(username, user);
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { DecodedToken, TokenPayload } from '../types/jwt';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import type { JwtSignOptions } from '@nestjs/jwt';
|
||||
import type { JwtSignOptions, JwtVerifyOptions } from '@nestjs/jwt';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { env } from 'src/config/env';
|
||||
@ -64,11 +64,11 @@ export class LdapService {
|
||||
}
|
||||
}
|
||||
|
||||
public async getUser(token: string) {
|
||||
public async getUser(token: string, options?: JwtVerifyOptions) {
|
||||
try {
|
||||
const { username } = this.jwtService.verify(token) as DecodedToken;
|
||||
const { username } = this.jwtService.verify(token, options) as DecodedToken;
|
||||
|
||||
const cachedUser = (await this.cacheManager.get(username)) as ldap.User;
|
||||
const cachedUser = await this.cacheManager.get<ldap.User>(username);
|
||||
|
||||
if (!cachedUser) {
|
||||
const user = await ldap.authenticate(username);
|
||||
|
||||
@ -71,7 +71,34 @@ function BaseForm({ children, onSubmit }: FormProps & PropsWithChildren) {
|
||||
|
||||
export const Form = {
|
||||
Default() {
|
||||
const { dispatch } = useContext(FormStateContext);
|
||||
const {
|
||||
dispatch,
|
||||
state: { step, user },
|
||||
} = useContext(FormStateContext);
|
||||
|
||||
function handleRefreshToken() {
|
||||
axios
|
||||
.get('/refresh-token')
|
||||
.then(() => redirect())
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_SERVER },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'login' && user) {
|
||||
return (
|
||||
<button
|
||||
className={styles['button-submit']}
|
||||
type="submit"
|
||||
onClick={() => handleRefreshToken()}
|
||||
>
|
||||
Продолжить как <b>{user?.displayName}</b>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function handleLogin(data: FormData) {
|
||||
return axios
|
||||
@ -147,7 +174,6 @@ export const Form = {
|
||||
}
|
||||
|
||||
function handleTelegramLogin() {
|
||||
// window.open(TELEGRAM_BOT_URL);
|
||||
axios
|
||||
.post<LdapUser>('/login-telegram')
|
||||
.then(() => {
|
||||
@ -172,6 +198,31 @@ export const Form = {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
function handleRefreshToken() {
|
||||
axios
|
||||
.get('/refresh-token')
|
||||
.then(() => redirect())
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_SERVER },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'login' && user) {
|
||||
return (
|
||||
<button
|
||||
className={styles['button-submit']}
|
||||
type="submit"
|
||||
onClick={() => handleRefreshToken()}
|
||||
>
|
||||
Продолжить как <b>{user?.displayName}</b>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'telegram') {
|
||||
return (
|
||||
<BaseForm onSubmit={() => handleTelegramLogin()}>
|
||||
|
||||
@ -4,6 +4,7 @@ const envSchema = z.object({
|
||||
APP_BASE_PATH: z.string().optional().default(''),
|
||||
APP_DESCRIPTION: z.string(),
|
||||
TELEGRAM_BOT_URL: z.string(),
|
||||
URL_API_CHECK_AUTH: z.string().default('http://auth_api:3001/check-auth'),
|
||||
});
|
||||
|
||||
module.exports = envSchema;
|
||||
|
||||
@ -57,11 +57,15 @@ type Context = {
|
||||
|
||||
export const FormStateContext = createContext<Context>({} as Context);
|
||||
|
||||
export function FormStateProvider({ children }: PropsWithChildren) {
|
||||
type FormStateProviderProps = {
|
||||
readonly user?: LdapUser;
|
||||
} & PropsWithChildren;
|
||||
|
||||
export function FormStateProvider({ children, user = undefined }: FormStateProviderProps) {
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
error: undefined,
|
||||
step: 'login',
|
||||
user: undefined,
|
||||
user,
|
||||
});
|
||||
|
||||
const value = useMemo(() => ({ dispatch, state }), [state]);
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"axios": "^1.5.1",
|
||||
"modern-normalize": "^2.0.0",
|
||||
"next": "^14.2.3",
|
||||
"radash": "^11.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.51.3",
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { Login } from '@/components';
|
||||
import { publicRuntimeConfig } from '@/config/runtime';
|
||||
import { publicRuntimeConfig, serverRuntimeConfig } from '@/config/runtime';
|
||||
import { FormStateProvider } from '@/context/form-state';
|
||||
import axios from 'axios';
|
||||
import Head from 'next/head';
|
||||
import { pick } from 'radash';
|
||||
|
||||
const { URL_API_CHECK_AUTH } = serverRuntimeConfig;
|
||||
const { APP_DESCRIPTION } = publicRuntimeConfig;
|
||||
|
||||
function PageHead() {
|
||||
@ -14,11 +17,31 @@ function PageHead() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
export default function Page(props) {
|
||||
return (
|
||||
<FormStateProvider>
|
||||
<FormStateProvider {...props}>
|
||||
<PageHead />
|
||||
<Login />
|
||||
</FormStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/** @type {import('next').GetServerSideProps} */
|
||||
export async function getServerSideProps({ req }) {
|
||||
try {
|
||||
const headers = pick(req.headers, ['auth-mode', 'cookie', 'refresh-token']);
|
||||
const { data: user } = await axios.get(URL_API_CHECK_AUTH, {
|
||||
headers,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
user,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,11 +14,13 @@ function PageHead() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
export default function Page(props) {
|
||||
return (
|
||||
<FormStateProvider>
|
||||
<FormStateProvider {...props}>
|
||||
<PageHead />
|
||||
<Login tfa />
|
||||
</FormStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export { getServerSideProps } from '.';
|
||||
|
||||
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@ -195,6 +195,9 @@ importers:
|
||||
next:
|
||||
specifier: ^14.2.3
|
||||
version: 14.2.3(@babel/core@7.23.3)(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5)
|
||||
radash:
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
@ -219,7 +222,7 @@ importers:
|
||||
devDependencies:
|
||||
'@vchikalkin/eslint-config-awesome':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6(@babel/eslint-plugin@7.22.10)(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(@types/node@20.10.0)(eslint-plugin-import@2.29.0)(eslint@8.54.0)(graphql@16.8.1)(prettier@3.2.5)(typescript@5.3.2)
|
||||
version: 1.1.6(@babel/eslint-plugin@7.22.10)(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(@types/node@20.10.0)(eslint-plugin-import@2.29.0)(eslint@8.54.0)(graphql@16.8.1)(prettier@3.3.1)(typescript@5.3.2)
|
||||
eslint:
|
||||
specifier: ^8.51.0
|
||||
version: 8.54.0
|
||||
@ -2510,7 +2513,7 @@ packages:
|
||||
- vitest
|
||||
dev: true
|
||||
|
||||
/@vchikalkin/eslint-config-awesome@1.1.6(@babel/eslint-plugin@7.22.10)(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(@types/node@20.10.0)(eslint-plugin-import@2.29.0)(eslint@8.54.0)(graphql@16.8.1)(prettier@3.2.5)(typescript@5.3.2):
|
||||
/@vchikalkin/eslint-config-awesome@1.1.6(@babel/eslint-plugin@7.22.10)(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(@types/node@20.10.0)(eslint-plugin-import@2.29.0)(eslint@8.54.0)(graphql@16.8.1)(prettier@3.3.1)(typescript@5.3.2):
|
||||
resolution: {integrity: sha512-GMgbUe9CupcCpQnvrZMalfwnuQwoYCH8mMYLYsBtVLbpjyzI3OVsX0GGvqPJbLDO1mBVH3H8DsMMFwwmOpP81A==}
|
||||
peerDependencies:
|
||||
'@babel/eslint-plugin': ^7.22.10
|
||||
@ -2522,7 +2525,7 @@ packages:
|
||||
eslint-config-canonical: 42.8.0(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(@types/node@20.10.0)(eslint@8.54.0)(graphql@16.8.1)(jest@29.7.0)(typescript@5.3.2)
|
||||
eslint-config-prettier: 9.0.0(eslint@8.54.0)
|
||||
eslint-plugin-canonical: 4.18.0(@typescript-eslint/parser@6.21.0)(eslint-plugin-import@2.29.0)(eslint@8.54.0)(typescript@5.3.2)
|
||||
eslint-plugin-prettier: 5.0.1(eslint-config-prettier@9.0.0)(eslint@8.54.0)(prettier@3.2.5)
|
||||
eslint-plugin-prettier: 5.0.1(eslint-config-prettier@9.0.0)(eslint@8.54.0)(prettier@3.3.1)
|
||||
eslint-plugin-sonarjs: 0.22.0(eslint@8.54.0)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/plugin-syntax-flow'
|
||||
@ -4596,6 +4599,27 @@ packages:
|
||||
synckit: 0.8.5
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.0.0)(eslint@8.54.0)(prettier@3.3.1):
|
||||
resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
'@types/eslint': '>=8.0.0'
|
||||
eslint: '>=8.0.0'
|
||||
eslint-config-prettier: '*'
|
||||
prettier: '>=3.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/eslint':
|
||||
optional: true
|
||||
eslint-config-prettier:
|
||||
optional: true
|
||||
dependencies:
|
||||
eslint: 8.54.0
|
||||
eslint-config-prettier: 9.0.0(eslint@8.54.0)
|
||||
prettier: 3.3.1
|
||||
prettier-linter-helpers: 1.0.0
|
||||
synckit: 0.8.5
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-promise@6.1.1(eslint@8.54.0):
|
||||
resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@ -7748,6 +7772,12 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/prettier@3.3.1:
|
||||
resolution: {integrity: sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/pretty-format@29.7.0:
|
||||
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user