feat(pages): add /application/[guildId]/[channelId]
(#4)
This commit is contained in:
26
tools/ajv.ts
Normal file
26
tools/ajv.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import Ajv from 'ajv'
|
||||
import addFormats from 'ajv-formats'
|
||||
|
||||
export const ajv = addFormats(
|
||||
new Ajv({
|
||||
allErrors: true
|
||||
}),
|
||||
[
|
||||
'date-time',
|
||||
'time',
|
||||
'date',
|
||||
'email',
|
||||
'hostname',
|
||||
'ipv4',
|
||||
'ipv6',
|
||||
'uri',
|
||||
'uri-reference',
|
||||
'uuid',
|
||||
'uri-template',
|
||||
'json-pointer',
|
||||
'relative-json-pointer',
|
||||
'regex'
|
||||
]
|
||||
)
|
||||
.addKeyword('kind')
|
||||
.addKeyword('modifier')
|
17
tools/api.ts
Normal file
17
tools/api.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export const API_VERSION = '1.0.0'
|
||||
|
||||
export const API_DEFAULT_PORT = 8080
|
||||
|
||||
export const API_URL =
|
||||
process.env.NEXT_PUBLIC_API_URL != null
|
||||
? process.env.NEXT_PUBLIC_API_URL
|
||||
: `http://localhost:${API_DEFAULT_PORT}`
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
106
tools/authentication/Authentication.ts
Normal file
106
tools/authentication/Authentication.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
|
||||
import { API_URL } from '../api'
|
||||
import { cookies } from '../cookies'
|
||||
import { Tokens } from '.'
|
||||
import { fetchRefreshToken } from './authenticationFromServerSide'
|
||||
|
||||
export class Authentication {
|
||||
public tokens: Tokens
|
||||
public accessTokenAge: number
|
||||
public socket: Socket
|
||||
public api: AxiosInstance
|
||||
|
||||
constructor(tokens: Tokens) {
|
||||
this.tokens = tokens
|
||||
this.accessTokenAge = Date.now()
|
||||
this.socket = io(API_URL, {
|
||||
auth: { token: `Bearer ${tokens.accessToken}` }
|
||||
})
|
||||
this.socket.on('connect_error', (error) => {
|
||||
if (error.message.startsWith('Unauthorized')) {
|
||||
fetchRefreshToken(this.tokens.refreshToken)
|
||||
.then(({ accessToken }) => {
|
||||
this.setAccessToken(accessToken)
|
||||
})
|
||||
.catch(async () => {
|
||||
this.signout()
|
||||
return await Promise.reject(error)
|
||||
})
|
||||
}
|
||||
})
|
||||
this.api = axios.create({
|
||||
baseURL: API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
this.api.interceptors.request.use(
|
||||
async (config) => {
|
||||
const isValidAccessToken =
|
||||
this.accessTokenAge + tokens.expiresIn > Date.now()
|
||||
if (!isValidAccessToken) {
|
||||
const { accessToken } = await fetchRefreshToken(
|
||||
this.tokens.refreshToken
|
||||
)
|
||||
this.setAccessToken(accessToken)
|
||||
}
|
||||
config.headers = config.headers == null ? {} : config.headers
|
||||
config.headers.Authorization = `${this.tokens.type} ${this.tokens.accessToken}`
|
||||
return config
|
||||
},
|
||||
async (error) => {
|
||||
this.signout()
|
||||
return await Promise.reject(error)
|
||||
}
|
||||
)
|
||||
this.api.interceptors.response.use(
|
||||
(response) => {
|
||||
return response
|
||||
},
|
||||
async (error) => {
|
||||
if (error.response.status !== 403 || (error.config._retry as boolean)) {
|
||||
return await Promise.reject(error)
|
||||
}
|
||||
error.config._retry = true
|
||||
try {
|
||||
const { accessToken } = await fetchRefreshToken(
|
||||
this.tokens.refreshToken
|
||||
)
|
||||
this.setAccessToken(accessToken)
|
||||
error.response.config.headers.Authorization = `${this.tokens.type} ${this.tokens.accessToken}`
|
||||
return await this.api.request(error.response.config)
|
||||
} catch {
|
||||
this.signout()
|
||||
return await Promise.reject(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public setAccessToken(accessToken: string): void {
|
||||
this.tokens.accessToken = accessToken
|
||||
this.accessTokenAge = Date.now()
|
||||
const token = `${this.tokens.type} ${this.tokens.accessToken}`
|
||||
if (typeof this.socket.auth !== 'function') {
|
||||
this.socket.auth.token = token
|
||||
}
|
||||
}
|
||||
|
||||
public signout(): void {
|
||||
cookies.remove('refreshToken')
|
||||
window.location.href = '/authentication/signin'
|
||||
}
|
||||
|
||||
public async signoutServerSide(): Promise<void> {
|
||||
await this.api.post('/users/signout', {
|
||||
refreshToken: this.tokens.refreshToken
|
||||
})
|
||||
this.signout()
|
||||
}
|
||||
|
||||
public signin(): void {
|
||||
cookies.set('refreshToken', this.tokens.refreshToken)
|
||||
}
|
||||
}
|
42
tools/authentication/AuthenticationContext.tsx
Normal file
42
tools/authentication/AuthenticationContext.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { createContext, useState, useEffect, useMemo, useContext } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import setLanguage from 'next-translate/setLanguage'
|
||||
|
||||
import { Authentication, PagePropsWithAuthentication } from '.'
|
||||
import { UserCurrent } from '../../models/User'
|
||||
|
||||
export interface AuthenticationValue {
|
||||
authentication: Authentication
|
||||
user: UserCurrent
|
||||
}
|
||||
|
||||
const defaultAnthenticationContext: AuthenticationValue = {} as any
|
||||
const AuthenticationContext = createContext<AuthenticationValue>(
|
||||
defaultAnthenticationContext
|
||||
)
|
||||
|
||||
export const AuthenticationProvider: React.FC<PagePropsWithAuthentication> = (
|
||||
props
|
||||
) => {
|
||||
const { setTheme } = useTheme()
|
||||
const [user] = useState<UserCurrent>(props.authentication.user)
|
||||
|
||||
const authentication = useMemo(() => {
|
||||
return new Authentication(props.authentication.tokens)
|
||||
}, [props.authentication.tokens])
|
||||
|
||||
useEffect(() => {
|
||||
setLanguage(user.settings.language).catch(() => {})
|
||||
setTheme(user.settings.theme)
|
||||
}, [setTheme, user.settings.language, user.settings.theme])
|
||||
|
||||
return (
|
||||
<AuthenticationContext.Provider value={{ authentication, user }}>
|
||||
{props.children}
|
||||
</AuthenticationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useAuthentication = (): AuthenticationValue => {
|
||||
return useContext(AuthenticationContext)
|
||||
}
|
99
tools/authentication/authenticationFromServerSide.ts
Normal file
99
tools/authentication/authenticationFromServerSide.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { AxiosInstance, AxiosResponse } from 'axios'
|
||||
import { GetServerSideProps, GetServerSidePropsContext } from 'next'
|
||||
|
||||
import { api } from '../api'
|
||||
import { Cookies } from '../cookies'
|
||||
import { RefreshTokenResponse, Tokens } from './index'
|
||||
import { Authentication } from './Authentication'
|
||||
import { UserCurrent } from '../../models/User'
|
||||
|
||||
export const fetchRefreshToken = async (
|
||||
refreshToken: string
|
||||
): Promise<Tokens> => {
|
||||
const { data } = await api.post<RefreshTokenResponse>(
|
||||
'/users/refresh-token',
|
||||
{
|
||||
refreshToken
|
||||
}
|
||||
)
|
||||
return { ...data, refreshToken }
|
||||
}
|
||||
|
||||
interface AuthenticationFromServerSideOptions {
|
||||
shouldBeAuthenticated: boolean
|
||||
|
||||
/** allows to fetch data server side with the authenticated user, the callback should returns the data that will be transfer to the component as props */
|
||||
fetchData?: (
|
||||
context: GetServerSidePropsContext,
|
||||
api: AxiosInstance
|
||||
) => Promise<{ [key: string]: any }>
|
||||
}
|
||||
|
||||
export const authenticationFromServerSide = (
|
||||
options: AuthenticationFromServerSideOptions
|
||||
): GetServerSideProps => {
|
||||
const { shouldBeAuthenticated, fetchData } = options
|
||||
|
||||
return async (context) => {
|
||||
const cookies = new Cookies(context.req.headers.cookie)
|
||||
const refreshToken = cookies.get('refreshToken')
|
||||
let tokens: Tokens | null = null
|
||||
if (refreshToken != null) {
|
||||
try {
|
||||
tokens = await fetchRefreshToken(refreshToken)
|
||||
} catch {
|
||||
cookies.remove('refreshToken')
|
||||
}
|
||||
}
|
||||
if (!shouldBeAuthenticated) {
|
||||
if (tokens != null) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/application',
|
||||
permanent: false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let data = {}
|
||||
if (fetchData != null) {
|
||||
data = await fetchData(context, api)
|
||||
}
|
||||
return { props: data }
|
||||
}
|
||||
} else {
|
||||
if (tokens == null) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/authentication/signin',
|
||||
permanent: false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
let data: any = {}
|
||||
const authentication = new Authentication(tokens)
|
||||
const { data: currentUser } = await authentication.api.get<
|
||||
unknown,
|
||||
AxiosResponse<UserCurrent>
|
||||
>('/users/current')
|
||||
if (fetchData != null) {
|
||||
data = await fetchData(context, authentication.api)
|
||||
}
|
||||
if (data.redirect != null) {
|
||||
return data
|
||||
}
|
||||
return {
|
||||
props: { authentication: { tokens, ...currentUser }, ...data }
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/404',
|
||||
permanent: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
tools/authentication/index.ts
Normal file
24
tools/authentication/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { UserCurrent } from '../../models/User'
|
||||
|
||||
export interface RefreshTokenResponse {
|
||||
accessToken: string
|
||||
|
||||
/** how long, in milliseconds, until the accessToken expires */
|
||||
expiresIn: number
|
||||
type: 'Bearer'
|
||||
}
|
||||
|
||||
export interface Tokens extends RefreshTokenResponse {
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
export interface PagePropsWithAuthentication {
|
||||
authentication: {
|
||||
tokens: Tokens
|
||||
user: UserCurrent
|
||||
}
|
||||
}
|
||||
|
||||
export * from './Authentication'
|
||||
export * from './authenticationFromServerSide'
|
||||
export * from './AuthenticationContext'
|
29
tools/cookies.ts
Normal file
29
tools/cookies.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import UniversalCookie from 'universal-cookie'
|
||||
|
||||
export class Cookies {
|
||||
/** how long in seconds, until the cookie expires (10 years) */
|
||||
static readonly COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
|
||||
|
||||
public universalCookie: UniversalCookie
|
||||
|
||||
constructor(cookies?: string | object | null) {
|
||||
this.universalCookie = new UniversalCookie(cookies)
|
||||
}
|
||||
|
||||
set(name: string, value: string): void {
|
||||
this.universalCookie.set(name, value, {
|
||||
path: '/',
|
||||
maxAge: Cookies.COOKIE_MAX_AGE
|
||||
})
|
||||
}
|
||||
|
||||
remove(name: string): void {
|
||||
this.universalCookie.remove(name, { path: '/' })
|
||||
}
|
||||
|
||||
get(name: string): string {
|
||||
return this.universalCookie.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
export const cookies = new Cookies()
|
10
tools/utils/__test__/capitalize.test.ts
Normal file
10
tools/utils/__test__/capitalize.test.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { capitalize } from '../capitalize'
|
||||
|
||||
describe('tools/utils/capitalize', () => {
|
||||
it('should capitalize the first letter of a string', () => {
|
||||
expect(capitalize('hello')).toEqual('Hello')
|
||||
expect(capitalize('HeLlo')).toEqual('HeLlo')
|
||||
expect(capitalize('member(s)')).toEqual('Member(s)')
|
||||
expect(capitalize('Member(s)')).toEqual('Member(s)')
|
||||
})
|
||||
})
|
3
tools/utils/capitalize.ts
Normal file
3
tools/utils/capitalize.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const capitalize = (string: string): string => {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
Reference in New Issue
Block a user