fix: file upload and OAuth2 (#26)

This commit is contained in:
Divlo 2022-04-08 20:59:04 +02:00 committed by GitHub
parent a64325f5b8
commit 313cfeeb36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 116 additions and 58 deletions

View File

@ -12,7 +12,6 @@ import { Guilds } from './Guilds/Guilds'
import { Divider } from '../design/Divider' import { Divider } from '../design/Divider'
import { Members } from './Members' import { Members } from './Members'
import { useAuthentication } from '../../tools/authentication' import { useAuthentication } from '../../tools/authentication'
import { API_URL } from '../../tools/api'
export interface ChannelsPath { export interface ChannelsPath {
channelId: number channelId: number
@ -189,7 +188,7 @@ export const Application: React.FC<ApplicationProps> = (props) => {
src={ src={
user.logo == null user.logo == null
? '/images/data/user-default.png' ? '/images/data/user-default.png'
: API_URL + user.logo : user.logo
} }
alt={"Users's profil picture"} alt={"Users's profil picture"}
draggable={false} draggable={false}

View File

@ -9,7 +9,6 @@ import useTranslation from 'next-translate/useTranslation'
import { HandleSubmitCallback, useForm } from '../../../hooks/useForm' import { HandleSubmitCallback, useForm } from '../../../hooks/useForm'
import { guildSchema } from '../../../models/Guild' import { guildSchema } from '../../../models/Guild'
import { FormState } from '../../design/FormState' import { FormState } from '../../design/FormState'
import { API_URL } from '../../../tools/api'
import { useGuildMember } from '../../../contexts/GuildMember' import { useGuildMember } from '../../../contexts/GuildMember'
import { Textarea } from '../../design/Textarea' import { Textarea } from '../../design/Textarea'
import { Input } from '../../design/Input' import { Input } from '../../design/Input'
@ -135,7 +134,7 @@ export const GuildSettings: React.FC = () => {
src={ src={
guild.icon == null guild.icon == null
? '/images/data/guild-default.png' ? '/images/data/guild-default.png'
: API_URL + guild.icon : guild.icon
} }
alt='Profil Picture' alt='Profil Picture'
draggable='false' draggable='false'

View File

@ -1,6 +1,5 @@
import Image from 'next/image' import Image from 'next/image'
import { API_URL } from '../../../../tools/api'
import { GuildWithDefaultChannelId } from '../../../../models/Guild' import { GuildWithDefaultChannelId } from '../../../../models/Guild'
import { IconLink } from '../../../design/IconLink' import { IconLink } from '../../../design/IconLink'
@ -24,9 +23,7 @@ export const Guild: React.FC<GuildProps> = (props) => {
quality={100} quality={100}
className='rounded-full' className='rounded-full'
src={ src={
guild.icon != null guild.icon != null ? guild.icon : '/images/data/guild-default.png'
? API_URL + guild.icon
: '/images/data/guild-default.png'
} }
alt='logo' alt='logo'
width={48} width={48}

View File

@ -7,7 +7,6 @@ import axios from 'axios'
import { Emoji } from '../../../Emoji' import { Emoji } from '../../../Emoji'
import { ConfirmGuildJoin } from '../../ConfirmGuildJoin' import { ConfirmGuildJoin } from '../../ConfirmGuildJoin'
import { API_URL } from '../../../../tools/api'
import { import {
GuildPublic as GuildPublicType, GuildPublic as GuildPublicType,
GuildWithDefaultChannelId GuildWithDefaultChannelId
@ -64,9 +63,7 @@ export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
quality={100} quality={100}
className='rounded-full' className='rounded-full'
src={ src={
guild.icon != null guild.icon != null ? guild.icon : '/images/data/guild-default.png'
? API_URL + guild.icon
: '/images/data/guild-default.png'
} }
alt='logo' alt='logo'
width={80} width={80}

View File

@ -2,7 +2,6 @@ import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { MemberWithPublicUser } from '../../../../models/Member' import { MemberWithPublicUser } from '../../../../models/Member'
import { API_URL } from '../../../../tools/api'
export interface MemberProps { export interface MemberProps {
member: MemberWithPublicUser member: MemberWithPublicUser
@ -20,7 +19,7 @@ export const Member: React.FC<MemberProps> = (props) => {
src={ src={
member.user.logo == null member.user.logo == null
? '/images/data/user-default.png' ? '/images/data/user-default.png'
: API_URL + member.user.logo : member.user.logo
} }
alt={"Users's profil picture"} alt={"Users's profil picture"}
height={50} height={50}

View File

@ -3,7 +3,6 @@ import Link from 'next/link'
import date from 'date-and-time' import date from 'date-and-time'
import { MessageWithMember } from '../../../../models/Message' import { MessageWithMember } from '../../../../models/Message'
import { API_URL } from '../../../../tools/api'
import { MessageText } from './MessageText' import { MessageText } from './MessageText'
import { Loader } from '../../../design/Loader' import { Loader } from '../../../design/Loader'
import { MessageFile } from './MessageFile' import { MessageFile } from './MessageFile'
@ -30,7 +29,7 @@ export const Message: React.FC<MessageProps> = (props) => {
src={ src={
message.member.user.logo == null message.member.user.logo == null
? '/images/data/user-default.png' ? '/images/data/user-default.png'
: API_URL + message.member.user.logo : message.member.user.logo
} }
alt={"Users's profil picture"} alt={"Users's profil picture"}
width={50} width={50}

View File

@ -3,10 +3,10 @@ import axios from 'axios'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import { DownloadIcon } from '@heroicons/react/solid' import { DownloadIcon } from '@heroicons/react/solid'
import { useAuthentication } from '../../../../../tools/authentication'
import { MessageWithMember } from '../../../../../models/Message' import { MessageWithMember } from '../../../../../models/Message'
import { Loader } from '../../../../design/Loader' import { Loader } from '../../../../design/Loader'
import { FileIcon } from './FileIcon' import { FileIcon } from './FileIcon'
import { api } from '../../../../../tools/api'
const supportedImageMimetype = [ const supportedImageMimetype = [
'image/png', 'image/png',
@ -27,14 +27,13 @@ export interface MessageContentProps {
export const MessageFile: React.FC<MessageContentProps> = (props) => { export const MessageFile: React.FC<MessageContentProps> = (props) => {
const { message } = props const { message } = props
const { authentication } = useAuthentication()
const [file, setFile] = useState<FileData | null>(null) const [file, setFile] = useState<FileData | null>(null)
useEffect(() => { useEffect(() => {
const ourRequest = axios.CancelToken.source() const ourRequest = axios.CancelToken.source()
const fetchData = async (): Promise<void> => { const fetchData = async (): Promise<void> => {
const { data } = await authentication.api.get(message.value, { const { data } = await api.get(message.value, {
responseType: 'blob', responseType: 'blob',
cancelToken: ourRequest.token cancelToken: ourRequest.token
}) })
@ -46,7 +45,7 @@ export const MessageFile: React.FC<MessageContentProps> = (props) => {
return () => { return () => {
ourRequest.cancel() ourRequest.cancel()
} }
}, [message.value, authentication.api]) }, [message.value])
if (file == null) { if (file == null) {
return <Loader /> return <Loader />

View File

@ -2,7 +2,6 @@ import Image from 'next/image'
import date from 'date-and-time' import date from 'date-and-time'
import useTranslation from 'next-translate/useTranslation' import useTranslation from 'next-translate/useTranslation'
import { API_URL } from '../../../tools/api'
import { UserPublic } from '../../../models/User' import { UserPublic } from '../../../models/User'
import { Guild } from '../../../models/Guild' import { Guild } from '../../../models/Guild'
@ -28,7 +27,7 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
className='rounded-full' className='rounded-full'
src={ src={
user.logo != null user.logo != null
? API_URL + user.logo ? user.logo
: '/images/data/user-default.png' : '/images/data/user-default.png'
} }
alt='Profil Picture' alt='Profil Picture'
@ -91,9 +90,7 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
</div> </div>
</div> </div>
<div className='mt-7'> <div className='mt-7'>
{user.biography != null && ( {user.biography != null && <p>{user.biography}</p>}
<p className='w-[45%]'>{user.biography}</p>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,13 +1,12 @@
import Image from 'next/image' import Image from 'next/image'
import useTranslation from 'next-translate/useTranslation' import useTranslation from 'next-translate/useTranslation'
import { useState } from 'react' import { useState, useMemo } from 'react'
import { Form } from 'react-component-form' import { Form } from 'react-component-form'
import { EyeIcon, PhotographIcon } from '@heroicons/react/solid' import { EyeIcon, PhotographIcon } from '@heroicons/react/solid'
import { Type } from '@sinclair/typebox' import { Type } from '@sinclair/typebox'
import axios from 'axios' import axios from 'axios'
import Link from 'next/link' import Link from 'next/link'
import { API_URL } from '../../../tools/api'
import { Input } from '../../design/Input' import { Input } from '../../design/Input'
import { Checkbox } from '../../design/Checkbox' import { Checkbox } from '../../design/Checkbox'
import { Textarea } from '../../design/Textarea' import { Textarea } from '../../design/Textarea'
@ -20,6 +19,7 @@ import { FormState } from '../../design/FormState'
import { useForm, HandleSubmitCallback } from '../../../hooks/useForm' import { useForm, HandleSubmitCallback } from '../../../hooks/useForm'
import { userSchema } from '../../../models/User' import { userSchema } from '../../../models/User'
import { userSettingsSchema } from '../../../models/UserSettings' import { userSettingsSchema } from '../../../models/UserSettings'
import { ProviderOAuth, providers } from '../../../models/OAuth'
export const UserSettings: React.FC = () => { export const UserSettings: React.FC = () => {
const { user, setUser, authentication } = useAuthentication() const { user, setUser, authentication } = useAuthentication()
@ -56,6 +56,10 @@ export const UserSettings: React.FC = () => {
resetOnSuccess: false resetOnSuccess: false
}) })
const hasAllProviders = useMemo(() => {
return providers.every((provider) => user.strategies.includes(provider))
}, [user.strategies])
const onSubmit: HandleSubmitCallback = async (formData) => { const onSubmit: HandleSubmitCallback = async (formData) => {
try { try {
const { isPublicGuilds, isPublicEmail, ...userData } = formData const { isPublicGuilds, isPublicEmail, ...userData } = formData
@ -173,6 +177,41 @@ export const UserSettings: React.FC = () => {
} }
} }
const handleDeletionProvider = (
provider: ProviderOAuth
): (() => Promise<void>) => {
return async () => {
try {
setFetchState('loading')
await authentication.api.delete(`/users/oauth2/${provider}`)
setUser((oldUser) => {
return {
...oldUser,
strategies: oldUser.strategies.filter(
(strategy) => strategy !== provider
)
}
})
setMessageTranslationKey('application:success-deleted-provider')
} catch (error) {
setFetchState('error')
setMessageTranslationKey('errors:server-error')
}
}
}
const handleAddProvider = (
provider: ProviderOAuth
): (() => Promise<void>) => {
return async () => {
const redirect = window.location.href.replace(location.search, '')
const { data: url } = await authentication.api.get(
`/users/oauth2/${provider.toLowerCase()}/add-strategy?redirectURI=${redirect}`
)
window.location.href = url
}
}
return ( return (
<Form <Form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
@ -197,7 +236,7 @@ export const UserSettings: React.FC = () => {
className='rounded-full opacity-50' className='rounded-full opacity-50'
src={ src={
user.logo != null user.logo != null
? API_URL + user.logo ? user.logo
: '/images/data/user-default.png' : '/images/data/user-default.png'
} }
alt='Profil Picture' alt='Profil Picture'
@ -264,7 +303,7 @@ export const UserSettings: React.FC = () => {
/> />
</div> </div>
<div className='flex h-full w-4/5 flex-col items-center justify-between pr-0 sm:w-[415px] lg:pl-12'> <div className='flex h-full w-4/5 flex-col items-center justify-between pr-0 sm:w-[415px] lg:pl-12'>
<div className='flex w-full items-center pt-14'> <div className='flex w-full items-center pt-10 lg:pt-0'>
<Language className='!top-12' /> <Language className='!top-12' />
<div className='ml-auto flex'> <div className='ml-auto flex'>
<SwitchTheme /> <SwitchTheme />
@ -283,18 +322,46 @@ export const UserSettings: React.FC = () => {
</div> </div>
<div className='mt-14 flex w-full flex-col gap-4'> <div className='mt-14 flex w-full flex-col gap-4'>
{!hasAllProviders ? (
<div className='flex w-full flex-col gap-4'>
<h3 className='text-center'>
{t('application:signin-with-an-account')}
</h3>
{providers.map((provider, index) => {
if (!user.strategies.includes(provider)) {
return (
<SocialMediaButton <SocialMediaButton
socialMedia='Google' key={index}
socialMedia={provider}
className='w-full justify-center' className='w-full justify-center'
onClick={handleAddProvider(provider)}
/> />
)
}
return null
})}
</div>
) : null}
{user.strategies.length !== 1 && (
<div className='mt-4 flex w-full flex-col gap-4'>
<h3 className='text-center'>
{t('application:signout-with-an-account')}
</h3>
{providers.map((provider, index) => {
if (user.strategies.includes(provider)) {
return (
<SocialMediaButton <SocialMediaButton
socialMedia='Discord' key={index}
className='w-full justify-center' socialMedia={provider}
/>
<SocialMediaButton
socialMedia='GitHub'
className='w-full justify-center' className='w-full justify-center'
onClick={handleDeletionProvider(provider)}
/> />
)
}
return null
})}
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,6 +4,7 @@ import { useRouter } from 'next/router'
import { api } from '../../tools/api' import { api } from '../../tools/api'
import { Authentication, isTokens } from '../../tools/authentication' import { Authentication, isTokens } from '../../tools/authentication'
import { SocialMediaButton, SocialMedia } from '../design/SocialMediaButton' import { SocialMediaButton, SocialMedia } from '../design/SocialMediaButton'
import { providers } from '../../models/OAuth'
export const AuthenticationSocialMedia: React.FC = () => { export const AuthenticationSocialMedia: React.FC = () => {
const router = useRouter() const router = useRouter()
@ -12,7 +13,7 @@ export const AuthenticationSocialMedia: React.FC = () => {
socialMedia: SocialMedia socialMedia: SocialMedia
): (() => Promise<void>) => { ): (() => Promise<void>) => {
return async () => { return async () => {
const redirect = window.location.href const redirect = window.location.href.replace(location.search, '')
const { data: url } = await api.get( const { data: url } = await api.get(
`/users/oauth2/${socialMedia.toLowerCase()}/signin?redirectURI=${redirect}` `/users/oauth2/${socialMedia.toLowerCase()}/signin?redirectURI=${redirect}`
) )
@ -32,18 +33,15 @@ export const AuthenticationSocialMedia: React.FC = () => {
return ( return (
<div className='flex flex-col sm:w-full sm:items-center'> <div className='flex flex-col sm:w-full sm:items-center'>
<div className='flex flex-col items-center justify-center space-y-6 sm:w-4/6 sm:flex-row sm:space-x-6 sm:space-y-0'> <div className='flex flex-col items-center justify-center space-y-6 sm:w-4/6 sm:flex-row sm:space-x-6 sm:space-y-0'>
{providers.map((provider, index) => {
return (
<SocialMediaButton <SocialMediaButton
socialMedia='Google' key={index}
onClick={handleAuthentication('Google')} socialMedia={provider}
/> onClick={handleAuthentication(provider)}
<SocialMediaButton
socialMedia='GitHub'
onClick={handleAuthentication('GitHub')}
/>
<SocialMediaButton
socialMedia='Discord'
onClick={handleAuthentication('Discord')}
/> />
)
})}
</div> </div>
</div> </div>
) )

View File

@ -19,6 +19,9 @@
"success-email-changed": "Please check your emails to confirm your new email address. You are now signed out.", "success-email-changed": "Please check your emails to confirm your new email address. You are now signed out.",
"delete": "Delete", "delete": "Delete",
"signout": "Sign out", "signout": "Sign out",
"signin-with-an-account": "Sign in with an account",
"signout-with-an-account": "Sign out with an account",
"success-deleted-provider": "The provider has been deleted.",
"leave": "Leave", "leave": "Leave",
"start-chatting-kill-ghost": "Start chatting to kill this ghost!" "start-chatting-kill-ghost": "Start chatting to kill this ghost!"
} }

View File

@ -19,6 +19,9 @@
"success-email-changed": "Veuillez vérifier vos emails pour confirmer votre nouvelle adresse email. Vous êtes maintenant déconnecté.", "success-email-changed": "Veuillez vérifier vos emails pour confirmer votre nouvelle adresse email. Vous êtes maintenant déconnecté.",
"delete": "Supprimer", "delete": "Supprimer",
"signout": "Se déconnecter", "signout": "Se déconnecter",
"signin-with-an-account": "Se connecter avec un compte",
"signout-with-an-account": "Se déconnecter avec un compte",
"success-deleted-provider": "Le moyen de connexion a été supprimé.",
"leave": "Quitter", "leave": "Quitter",
"start-chatting-kill-ghost": "Commencez à discuter pour tuer ce fantôme !" "start-chatting-kill-ghost": "Commencez à discuter pour tuer ce fantôme !"
} }

View File

@ -9,6 +9,7 @@ module.exports = nextTranslate(
domains: [ domains: [
'api.thream.divlo.fr', 'api.thream.divlo.fr',
'thream-api.herokuapp.com', 'thream-api.herokuapp.com',
'file-uploads-api.thream.divlo.fr',
...(process.env.NODE_ENV !== 'production' ? ['localhost'] : []) ...(process.env.NODE_ENV !== 'production' ? ['localhost'] : [])
] ]
}, },

4
package-lock.json generated
View File

@ -91,8 +91,8 @@
"vercel": "24.0.1" "vercel": "24.0.1"
}, },
"engines": { "engines": {
"node": ">=16.0.0", "node": ">=14.0.0",
"npm": ">=8.0.0" "npm": ">=7.0.0"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {

View File

@ -7,8 +7,8 @@
"url": "https://github.com/Thream/website" "url": "https://github.com/Thream/website"
}, },
"engines": { "engines": {
"node": ">=16.0.0", "node": ">=14.0.0",
"npm": ">=8.0.0" "npm": ">=7.0.0"
}, },
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",