diff --git a/backend/api_gateway/src/main/kotlin/com/linkedout/backend/config/WebFluxConfiguration.kt b/backend/api_gateway/src/main/kotlin/com/linkedout/backend/config/WebFluxConfiguration.kt
new file mode 100644
index 0000000..f475f5a
--- /dev/null
+++ b/backend/api_gateway/src/main/kotlin/com/linkedout/backend/config/WebFluxConfiguration.kt
@@ -0,0 +1,12 @@
+package com.linkedout.backend.config
+
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.codec.ServerCodecConfigurer
+import org.springframework.web.reactive.config.WebFluxConfigurer
+
+@Configuration
+internal open class WebFluxConfiguration : WebFluxConfigurer {
+ override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
+ configurer.defaultCodecs().maxInMemorySize(8 * 1024 * 1024)
+ }
+}
diff --git a/backend/api_gateway/src/main/resources/application.yml b/backend/api_gateway/src/main/resources/application.yml
index 84bcc01..b759443 100644
--- a/backend/api_gateway/src/main/resources/application.yml
+++ b/backend/api_gateway/src/main/resources/application.yml
@@ -1,4 +1,6 @@
spring:
+ codec:
+ max-in-memory-size: 8MB
security:
oauth2:
resourceserver:
diff --git a/front/app.json b/front/app.json
index b15bf4c..fa11007 100644
--- a/front/app.json
+++ b/front/app.json
@@ -28,6 +28,11 @@
"experiments": {
"tsconfigPaths": true
},
- "plugins": ["expo-localization", "expo-font", "expo-secure-store"]
+ "plugins": [
+ "expo-localization",
+ "expo-font",
+ "expo-secure-store",
+ "expo-document-picker"
+ ]
}
}
diff --git a/front/assets/translations/en_US.json b/front/assets/translations/en_US.json
index 4fe53b8..ac28a45 100644
--- a/front/assets/translations/en_US.json
+++ b/front/assets/translations/en_US.json
@@ -69,7 +69,17 @@
"phoneNumber": "Phone number",
"email": "Email",
"references": "References",
- "downloadResume": "Download resume",
+ "uploadPicture": "Upload profile picture",
+ "uploadResume": "Upload resume",
+ "shareResume": "Share resume",
+ "resumeNotFound": {
+ "title": "Resume not found",
+ "message": "You have not uploaded a resume yet."
+ },
+ "resumeDownloadError": {
+ "title": "Download error",
+ "message": "An error occurred while downloading the resume."
+ },
"information": "Information",
"firstName": "First name",
"lastName": "Last name",
diff --git a/front/assets/translations/fr_FR.json b/front/assets/translations/fr_FR.json
index 34ea490..241536d 100644
--- a/front/assets/translations/fr_FR.json
+++ b/front/assets/translations/fr_FR.json
@@ -68,7 +68,17 @@
"phoneNumber": "Numéro de téléphone",
"email": "Email",
"references": "Références",
- "downloadResume": "Télécharger le CV",
+ "uploadPicture": "Modifier la photo de profil",
+ "uploadResume": "Modifier le CV",
+ "shareResume": "Partager le CV",
+ "resumeNotFound": {
+ "title": "Aucun CV trouvé",
+ "message": "Vous n'avez pas encore ajouté de CV à votre profil."
+ },
+ "resumeDownloadError": {
+ "title": "Erreur de téléchargement",
+ "message": "Une erreur est survenue lors du téléchargement de votre CV."
+ },
"information": "Informations",
"firstName": "Prénom",
"lastName": "Nom",
diff --git a/front/package-lock.json b/front/package-lock.json
index 596764b..568368d 100644
--- a/front/package-lock.json
+++ b/front/package-lock.json
@@ -20,9 +20,13 @@
"expo": "~50.0.6",
"expo-auth-session": "~5.4.0",
"expo-crypto": "~12.8.0",
+ "expo-document-picker": "~11.10.1",
+ "expo-file-system": "~16.0.6",
"expo-font": "~11.10.2",
+ "expo-image-picker": "~14.7.1",
"expo-localization": "~14.8.3",
"expo-secure-store": "~12.8.1",
+ "expo-sharing": "~11.10.0",
"expo-splash-screen": "~0.26.4",
"expo-status-bar": "~1.11.1",
"expo-system-ui": "~2.9.3",
@@ -9508,6 +9512,14 @@
"expo": "*"
}
},
+ "node_modules/expo-document-picker": {
+ "version": "11.10.1",
+ "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-11.10.1.tgz",
+ "integrity": "sha512-A1MiLfyXQ+KxanRO5lYxYQy3ryV+25JHe5Ai/BLV+FJU0QXByUF+Y/dn35WVPx5gpdZXC8UJ4ejg5SKSoeconw==",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-file-system": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-16.0.6.tgz",
@@ -9527,6 +9539,25 @@
"expo": "*"
}
},
+ "node_modules/expo-image-loader": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-4.6.0.tgz",
+ "integrity": "sha512-RHQTDak7/KyhWUxikn2yNzXL7i2cs16cMp6gEAgkHOjVhoCJQoOJ0Ljrt4cKQ3IowxgCuOrAgSUzGkqs7omj8Q==",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-image-picker": {
+ "version": "14.7.1",
+ "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-14.7.1.tgz",
+ "integrity": "sha512-ILQVOJgI3aEzrDmCFGDPtpAepYkn8mot8G7vfQ51BfFdQbzL6N3Wm1fS/ofdWlAZJl/qT2DwaIh5xYmf3SyGZA==",
+ "dependencies": {
+ "expo-image-loader": "~4.6.0"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-keep-awake": {
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-12.8.2.tgz",
@@ -9684,6 +9715,14 @@
"expo": "*"
}
},
+ "node_modules/expo-sharing": {
+ "version": "11.10.0",
+ "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-11.10.0.tgz",
+ "integrity": "sha512-/64RyyKlZ25WfnMXa87HbPXhIIqWwNbIku/RaIYAq4SE0XTRC+KTH3v0XFkfDa+SCG/jKsAr1pJ3vQvsNo1sCQ==",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-splash-screen": {
"version": "0.26.4",
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.26.4.tgz",
diff --git a/front/package.json b/front/package.json
index 93d1319..4efd79f 100644
--- a/front/package.json
+++ b/front/package.json
@@ -27,7 +27,9 @@
"expo-auth-session": "~5.4.0",
"expo-crypto": "~12.8.0",
"expo-font": "~11.10.2",
+ "expo-image-picker": "~14.7.1",
"expo-localization": "~14.8.3",
+ "expo-secure-store": "~12.8.1",
"expo-splash-screen": "~0.26.4",
"expo-status-bar": "~1.11.1",
"expo-system-ui": "~2.9.3",
@@ -44,7 +46,9 @@
"react-native-tab-view": "^3.5.2",
"react-redux": "^9.1.0",
"typescript": "^5.3.3",
- "expo-secure-store": "~12.8.1"
+ "expo-document-picker": "~11.10.1",
+ "expo-sharing": "~11.10.0",
+ "expo-file-system": "~16.0.6"
},
"devDependencies": {
"@babel/core": "^7.23.9",
diff --git a/front/src/components/profile/ProfileShareResumeButton.tsx b/front/src/components/profile/ProfileShareResumeButton.tsx
new file mode 100644
index 0000000..4bf4508
--- /dev/null
+++ b/front/src/components/profile/ProfileShareResumeButton.tsx
@@ -0,0 +1,64 @@
+import * as FileSystem from 'expo-file-system';
+import * as Sharing from 'expo-sharing';
+import { useCallback } from 'react';
+import { Alert } from 'react-native';
+import { Button } from 'react-native-paper';
+
+import { useAppSelector } from '@/store/hooks';
+import i18n from '@/utils/i18n';
+
+/**
+ * Button to share the resume of the user.
+ * @constructor
+ */
+export const ProfileShareResumeButton = () => {
+ // Store hooks
+ const auth = useAppSelector((state) => state.auth);
+
+ // Callbacks
+ const handleDownloadResume = useCallback(async () => {
+ if (auth.state !== 'authenticated') {
+ return;
+ }
+
+ const downloadResult = await FileSystem.downloadAsync(
+ `${process.env.EXPO_PUBLIC_API_URL}/profile/cv`,
+ FileSystem.cacheDirectory + 'cv.pdf',
+ {
+ headers: {
+ Authorization: `Bearer ${auth.token}`,
+ },
+ },
+ );
+
+ if (downloadResult.status !== 200) {
+ if (downloadResult.status === 404) {
+ Alert.alert(
+ i18n.t('profile.info.resumeNotFound.title'),
+ i18n.t('profile.info.resumeNotFound.message'),
+ );
+ } else {
+ Alert.alert(
+ i18n.t('profile.info.resumeDownloadError.title'),
+ i18n.t('profile.info.resumeDownloadError.message'),
+ );
+ }
+
+ return;
+ }
+
+ await Sharing.shareAsync(downloadResult.uri);
+ }, [auth]);
+
+ return (
+
+ );
+};
+
+export default ProfileShareResumeButton;
diff --git a/front/src/components/profile/ProfileUpdateInfosForm.tsx b/front/src/components/profile/ProfileUpdateInfosForm.tsx
index f18d482..8c3762c 100644
--- a/front/src/components/profile/ProfileUpdateInfosForm.tsx
+++ b/front/src/components/profile/ProfileUpdateInfosForm.tsx
@@ -2,6 +2,8 @@ import { FC, useCallback } from 'react';
import { Image, StyleSheet, View, ViewStyle } from 'react-native';
import { Text, TextInput } from 'react-native-paper';
+import ProfileUploadPictureButton from '@/components/profile/ProfileUploadPictureButton';
+import ProfileUploadResumeButton from '@/components/profile/ProfileUploadResumeButton';
import i18n from '@/utils/i18n';
/**
@@ -27,6 +29,10 @@ const styles = StyleSheet.create({
textInput: {
marginVertical: 8,
},
+ uploadButtonContainer: {
+ gap: 8,
+ marginVertical: 8,
+ },
});
/**
@@ -127,6 +133,11 @@ const ProfileUpdateInfosForm: FC = ({
onChangeText={(value) => handleInputChange('shortBiography', value)}
/>
+
+
+
+
+
{i18n.t('profile.info.contact')}
diff --git a/front/src/components/profile/ProfileUploadPictureButton.tsx b/front/src/components/profile/ProfileUploadPictureButton.tsx
new file mode 100644
index 0000000..06cd27b
--- /dev/null
+++ b/front/src/components/profile/ProfileUploadPictureButton.tsx
@@ -0,0 +1,43 @@
+import * as ImagePicker from 'expo-image-picker';
+import { useCallback } from 'react';
+import { Button } from 'react-native-paper';
+
+import { useUploadProfilePictureMutation } from '@/store/api/profileApiSlice';
+import i18n from '@/utils/i18n';
+
+/**
+ * Button to upload the profile picture of the user.
+ * @constructor
+ */
+export const ProfileUploadPictureButton = () => {
+ // API calls
+ const [uploadProfilePicture] = useUploadProfilePictureMutation();
+
+ // Callbacks
+ const handleUploadPicture = useCallback(async () => {
+ const result = await ImagePicker.launchImageLibraryAsync({
+ allowsEditing: true,
+ aspect: [1, 1],
+ exif: false,
+ quality: 0.4,
+ });
+
+ if (result.canceled === true || result.assets.length === 0) {
+ return;
+ }
+
+ uploadProfilePicture(result.assets[0].uri);
+ }, [uploadProfilePicture]);
+
+ return (
+
+ );
+};
+
+export default ProfileUploadPictureButton;
diff --git a/front/src/components/profile/ProfileUploadResumeButton.tsx b/front/src/components/profile/ProfileUploadResumeButton.tsx
new file mode 100644
index 0000000..645c9f1
--- /dev/null
+++ b/front/src/components/profile/ProfileUploadResumeButton.tsx
@@ -0,0 +1,40 @@
+import * as DocumentPicker from 'expo-document-picker';
+import { useCallback } from 'react';
+import { Button } from 'react-native-paper';
+
+import { useUploadResumeMutation } from '@/store/api/profileApiSlice';
+import i18n from '@/utils/i18n';
+
+/**
+ * Button to upload the resume of the user.
+ * @constructor
+ */
+export const ProfileUploadResumeButton = () => {
+ // API calls
+ const [uploadResume] = useUploadResumeMutation();
+
+ // Callbacks
+ const handleUploadResume = useCallback(async () => {
+ const result = await DocumentPicker.getDocumentAsync({
+ type: 'application/pdf',
+ });
+
+ if (result.canceled === true || result.assets.length === 0) {
+ return;
+ }
+
+ uploadResume(result.assets[0].uri);
+ }, [uploadResume]);
+
+ return (
+
+ );
+};
+
+export default ProfileUploadResumeButton;
diff --git a/front/src/components/profile/header/ProfileHeader.tsx b/front/src/components/profile/header/ProfileHeader.tsx
index 7f1632d..c05bc60 100644
--- a/front/src/components/profile/header/ProfileHeader.tsx
+++ b/front/src/components/profile/header/ProfileHeader.tsx
@@ -1,10 +1,9 @@
import { FC } from 'react';
import { StyleSheet, View } from 'react-native';
-import { Button } from 'react-native-paper';
+import ProfileShareResumeButton from '@/components/profile/ProfileShareResumeButton';
import ProfileHeaderDescription from '@/components/profile/header/ProfileHeaderDescription';
import ProfileHeaderName from '@/components/profile/header/ProfileHeaderName';
-import i18n from '@/utils/i18n';
/**
* The styles for the ProfileHeader component.
@@ -68,10 +67,7 @@ const ProfileHeader: FC = ({
/>
-
-
+
);
};
diff --git a/front/src/global.d.ts b/front/src/global.d.ts
new file mode 100644
index 0000000..dbaf068
--- /dev/null
+++ b/front/src/global.d.ts
@@ -0,0 +1,10 @@
+interface FormDataValue {
+ uri: string;
+ name: string;
+ type: string;
+}
+
+interface FormData {
+ append(name: string, value: FormDataValue, fileName?: string): void;
+ set(name: string, value: FormDataValue, fileName?: string): void;
+}
diff --git a/front/src/pages/profile/ProfilePage.tsx b/front/src/pages/profile/ProfilePage.tsx
index 1489f0d..b27d816 100644
--- a/front/src/pages/profile/ProfilePage.tsx
+++ b/front/src/pages/profile/ProfilePage.tsx
@@ -5,7 +5,10 @@ import { Appbar } from 'react-native-paper';
import ProfileContents from '@/components/profile/ProfileContents';
import { useGetAvailabilitiesQuery } from '@/store/api/availabilityApiSlice';
-import { useGetProfileQuery } from '@/store/api/profileApiSlice';
+import {
+ useGetProfilePictureQuery,
+ useGetProfileQuery,
+} from '@/store/api/profileApiSlice';
import { ProfileStackParamList } from './ProfileNav';
@@ -24,6 +27,7 @@ type ProfilePageProps = NativeStackScreenProps<
const ProfilePage: FC = ({ navigation }) => {
// API calls
const { data: profile, refetch: refetchProfile } = useGetProfileQuery();
+ const { data: profilePicture } = useGetProfilePictureQuery();
const { data: availabilities, refetch: refetchAvailabilities } =
useGetAvailabilitiesQuery();
@@ -71,7 +75,7 @@ const ProfilePage: FC = ({ navigation }) => {
return (
= ({ navigation }) => {
// API calls
const { data: profile } = useGetProfileQuery();
+ const { data: profilePicture } = useGetProfilePictureQuery();
const { data: availabilities } = useGetAvailabilitiesQuery();
const [patchProfile] = usePatchProfileMutation();
const [deleteAvailability] = useDeleteAvailabilityMutation();
@@ -143,7 +145,7 @@ const ProfileUpdatePage: FC = ({ navigation }) => {
contentContainerStyle={styles.contentContainer}
>
diff --git a/front/src/store/api/apiSlice.ts b/front/src/store/api/apiSlice.ts
index ad577ed..295ef37 100644
--- a/front/src/store/api/apiSlice.ts
+++ b/front/src/store/api/apiSlice.ts
@@ -29,6 +29,8 @@ export const apiSlice = createApi({
'Experiences',
'JobCategories',
'Profile',
+ 'ProfilePicture',
+ 'ProfileResume',
'References',
'Job',
'JobOffer',
diff --git a/front/src/store/api/profileApiSlice.ts b/front/src/store/api/profileApiSlice.ts
index 6e6a666..91edc02 100644
--- a/front/src/store/api/profileApiSlice.ts
+++ b/front/src/store/api/profileApiSlice.ts
@@ -28,6 +28,57 @@ export const extendedApiSlice = apiSlice.injectEndpoints({
}),
invalidatesTags: ['Profile'],
}),
+ getProfilePicture: builder.query({
+ query: () => ({
+ url: 'profile/photo',
+ responseHandler: async (response) => {
+ const blob = await response.blob();
+
+ return new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result);
+ reader.readAsDataURL(blob);
+ });
+ },
+ }),
+ providesTags: ['ProfilePicture'],
+ }),
+ uploadProfilePicture: builder.mutation({
+ query: (uri) => {
+ const formData = new FormData();
+ formData.append('file', {
+ uri,
+ name: 'profile-picture.png',
+ type: 'image/png',
+ });
+
+ return {
+ url: 'profile/photo',
+ method: 'POST',
+ body: formData,
+ formData: true,
+ };
+ },
+ invalidatesTags: ['ProfilePicture'],
+ }),
+ uploadResume: builder.mutation({
+ query: (uri) => {
+ const formData = new FormData();
+ formData.append('file', {
+ uri,
+ name: 'cv.pdf',
+ type: 'application/pdf',
+ });
+
+ return {
+ url: 'profile/cv',
+ method: 'POST',
+ body: formData,
+ formData: true,
+ };
+ },
+ invalidatesTags: ['ProfileResume'],
+ }),
}),
});
@@ -35,4 +86,7 @@ export const {
useGetProfileQuery,
usePatchProfileMutation,
usePutProfileMutation,
+ useGetProfilePictureQuery,
+ useUploadProfilePictureMutation,
+ useUploadResumeMutation,
} = extendedApiSlice;
diff --git a/kube/prod/api_gateway/configure-ingress.patch.yml b/kube/prod/api_gateway/configure-ingress.patch.yml
index 09c5de2..338c674 100644
--- a/kube/prod/api_gateway/configure-ingress.patch.yml
+++ b/kube/prod/api_gateway/configure-ingress.patch.yml
@@ -1,9 +1,6 @@
- op: add
path: /metadata/annotations/cert-manager.io~1cluster-issuer
value: letsencrypt-prod
-- op: add
- path: /spec/ingressClassName
- value: nginx
- op: replace
path: /spec/rules/0/host
value: linkedout.cluster-2020-5.dopolytech.fr