feat(pages): add /application/[guildId]/[channelId] (#4)

This commit is contained in:
Divlo
2022-01-01 20:42:25 +01:00
committed by GitHub
parent 91e246b759
commit fdc2a2d1de
118 changed files with 6040 additions and 2094 deletions

26
tools/ajv.ts Normal file
View 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
View 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'
}
})

View 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)
}
}

View 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)
}

View 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
}
}
}
}
}
}
}

View 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
View 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()

View 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)')
})
})

View File

@ -0,0 +1,3 @@
export const capitalize = (string: string): string => {
return string.charAt(0).toUpperCase() + string.slice(1)
}