8000 feat(files): register signed file urls in database (#193) by BryanBerger98 · Pull Request #194 · BryanBerger98/lodge-v2 · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat(files): register signed file urls in database (#193) #194

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/(app)/account/_components/UpdateAvatarForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const UpdateAvatarForm = ({ csrfToken }: UpdateAvatarFormProps) => {
<Avatar className="w-32 h-32">
<AvatarImage
alt="Profile"
src={ (fileToUpload && URL.createObjectURL(fileToUpload)) || currentUser?.photo_url || undefined }
src={ (fileToUpload && URL.createObjectURL(fileToUpload)) || currentUser?.photo?.url || undefined }
/>
<AvatarFallback><User /></AvatarFallback>
</Avatar>
Expand Down
6 changes: 3 additions & 3 deletions app/(app)/users/[user_id]/_components/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import ConfirmationModal from '@/components/ui/Modal/ConfirmationModal';
import { useToast } from '@/components/ui/use-toast';
import useAuth from '@/context/auth/useAuth';
import { deleteUser, sendResetPasswordTokenToUser, sendVerificationTokenToUser } from '@/services/users.service';
import { IUser } from '@/types/user.type';
import { IUserPopulated } from '@/types/user.type';
import { ApiError, getErrorMessage } from '@/utils/error';

import useUsers from '../../_context/users/useUsers';

type MenuProps = {
userData: IUser;
userData: IUserPopulated;
csrfToken: string;
}

Expand All @@ -34,7 +34,7 @@ type ModalState<T extends ('form' | 'simple')> = T extends 'form' ? {
action: 'reset-password' | 'verify-email';
};

const getModalContent = (userData: IUser) => ({
const getModalContent = (userData: IUserPopulated) => ({
delete: {
title: 'Delete user',
description: <span>Please enter the email of the user <span className="font-bold text-slate-700 select-none">{ userData.email }</span> to confirm the deletion. This action is irreversible.</span>,
Expand Down
13 changes: 8 additions & 5 deletions app/(app)/users/[user_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { redirect } from 'next/navigation';

import PageTitle from '@/components/layout/Header/PageTitle';
import BackButton from '@/components/ui/Button/BackButton';
import { findFileByKey } from '@/database/file/file.repository';
import { updateFileURL } from '@/database/file/file.repository';
import { findUserById } from '@/database/user/user.repository';
import { getFieldSignedURL } from '@/lib/bucket';
import { getCsrfToken } from '@/lib/csrf';
Expand Down Expand Up @@ -37,11 +37,14 @@ const EditUserPage = async ({ params }: EditUserPageProps) => {
redirect('/users');
}

if (userData.photo_key) {
const photoFileObject = await findFileByKey(userData.photo_key);
userData.photo_url = photoFileObject ? await getFieldSignedURL(photoFileObject.key, 24 * 60 * 60) : null;
if (userData.photo && userData.photo.url_expiration_date && userData.photo.url_expiration_date < new Date()) {
const photoUrl = await getFieldSignedURL(userData.photo.key, 24 * 60 * 60);
const updatedFile = await updateFileURL({
id: userData.photo.id,
url: photoUrl,
});
userData.photo = updatedFile;
}

return (
<>
<PageTitle><User /> Edit user</PageTitle>
Expand Down
6 changes: 3 additions & 3 deletions app/(app)/users/_components/EditUserForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Switch } from '@/components/ui/switch';
import { useToast } from '@/components/ui/use-toast';
import { createUser, updateUser } from '@/services/users.service';
import { IUser, UserRole } from '@/types/user.type';
import { IUserPopulated, UserRole } from '@/types/user.type';
import { ApiError, getErrorMessage } from '@/utils/error';

import useUsers from '../../_context/users/useUsers';

type EditUserFormProps = {
user?: IUser;
user?: IUserPopulated;
csrfToken: string;
};

Expand Down Expand Up @@ -142,7 +142,7 @@ const EditUserForm = ({ user, csrfToken }: EditUserFormProps) => {
<Avatar className="w-32 h-32">
<AvatarImage
alt="Profile"
src={ (fileToUpload && URL.createObjectURL(fileToUpload)) || user?.photo_url || undefined }
src={ (fileToUpload && URL.createObjectURL(fileToUpload)) || user?.photo?.url || undefined }
/>
<AvatarFallback><User /></AvatarFallback>
</Avatar>
Expand Down
20 changes: 16 additions & 4 deletions app/(app)/users/_components/UsersDataTable/columns.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { CheckedState } from '@radix-ui/react-checkbox';
import { ColumnDef } from '@tanstack/react-table';
import { AppleIcon, ArrowDown, ArrowUp, ArrowUpDown, BadgeCheck, BadgeX, KeyRound, Mail } from 'lucide-react';
import { AppleIcon, ArrowDown, ArrowUp, ArrowUpDown, BadgeCheck, BadgeX, KeyRound, Mail, User } from 'lucide-react';

import GoogleIcon from '@/components/icons/google';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { AuthProvider, IUser } from '@/types/user.type';
import { AuthProvider, IUserPopulated } from '@/types/user.type';

import RowMenu from './RowMenu';

export type UserColumn = IUser;
export type UserColumn = IUserPopulated;

export const COLUMN_NAMES = {
username: 'username',
Expand Down Expand Up @@ -80,7 +81,18 @@ export const columns: ColumnDef<UserColumn>[] = [
</Button>
);
},
cell: ({ row }) => row.original.username ? row.original.username : <span className="italic text-slate-500">No username</span>,
cell: ({ row }) => (
<span className="flex gap-2 items-center">
<Avatar className="w-8 h-8">
<AvatarImage
alt="Profile"
src={ row.original.photo?.url || undefined }
/>
<AvatarFallback><User size="16" /></AvatarFallback>
</Avatar>
{ row.original.username ? row.original.username : <span className="italic text-slate-500">No username</span> }
</span>
),
},
{
id: 'email',
Expand Down
4 changes: 2 additions & 2 deletions app/(app)/users/_context/users/users.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { ReactNode, useCallback, useEffect, useMemo, useReducer } from 'react';

import useFetchUsers from '@/hooks/users/useFetchUsers';
import { IUpdateUser, IUser } from '@/types/user.type';
import { IUpdateUser, IUserPopulated } from '@/types/user.type';
import { LoadingState, LoadingStateError } from '@/types/utils/loading.type';

import { SetUsersStatePayload, USERS_ERROR_ACTION, USERS_IDLE_ACTION, USERS_PENDING_ACTION, USERS_SET_STATE_ACTION, USERS_UPDATE_ACTION } from './users.actions';
Expand All @@ -20,7 +20,7 @@ const INITIAL_STATE: UsersState = {

type UsersProviderProps = {
children: ReactNode;
users?: IUser[],
users?: IUserPopulated[],
total?: number,
}

Expand Down
4 changes: 2 additions & 2 deletions app/(app)/users/_context/users/users.reducer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Reducer } from 'react';

import { IUser } from '@/types/user.type';
import { IUserPopulated } from '@/types/user.type';
import { LoadingState } from '@/types/utils/loading.type';

import { USERS_ERROR_ACTION, USERS_IDLE_ACTION, USERS_PENDING_ACTION, USERS_SET_STATE_ACTION, USERS_UPDATE_ACTION, UsersReducerAction } from './users.actions';

export type UsersState = {
users: IUser[];
users: IUserPopulated[];
total: number;
loading: LoadingState;
error?: string;
Expand Down
22 changes: 21 additions & 1 deletion app/(app)/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import { FetchUsersSchema } from '@/app/api/users/_schemas/fetch-users.schema';
import PageTitle from '@/components/layout/Header/PageTitle';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { updateFileURL } from '@/database/file/file.repository';
import { findUsers, findUsersCount } from '@/database/user/user.repository';
import { getFieldSignedURL } from '@/lib/bucket';
import { getCsrfToken } from '@/lib/csrf';
import { connectToDatabase } from '@/lib/database';
import { isFileURLExpired } from '@/utils/file.util';

import UsersProvider from './_context/users/users.provider';

Expand All @@ -31,11 +34,28 @@ const UsersPage = async ({ searchParams }: UsersPageProps) => {
const searchRegexArray = searchArray.map(string => new RegExp(string, 'i'));
const searchRequest = searchRegexArray.length > 0 ? { $or: [ { username: { $in: searchRegexArray } }, { email: { $in: searchRegexArray } } ] } : {};

const users = await findUsers(searchRequest, {
let users = await findUsers(searchRequest, {
sort: Object.fromEntries(sort_fields.map((field, index) => [ field, sort_directions[ index ] as 1 | -1 ])),
skip: Math.round(page_index * page_size),
limit: page_size,
});

const expiredFiles = isFileURLExpired(...users.map(user => user.photo));

if (expiredFiles.length > 0) {
await Promise.all(expiredFiles.map(async (file) => {
const photoUrl = await getFieldSignedURL(file.key, 24 * 60 * 60);
await updateFileURL({
id: file.id,
url: photoUrl,
});
}));
users = await findUsers(searchRequest, {
sort: Object.fromEntries(sort_fields.map((field, index) => [ field, sort_directions[ index ] as 1 | -1 ])),
skip: Math.round(page_index * page_size),
limit: page_size,
});
}

const totalUsers = await findUsersCount(searchRequest);
const disabledUsersCount = await findUsersCount({
Expand Down
65 changes: 27 additions & 38 deletions app/api/auth/account/avatar/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { NextResponse, type NextRequest } from 'next/server';

import { createFile, deleteFileById, findFileByKey } from '@/database/file/file.repository';
import { createFile, deleteFileById, findFileById } from '@/database/file/file.repository';
import { findUserById, updateUser } from '@/database/user/user.repository';
import { deleteFileFromKey, getFieldSignedURL, uploadImageToS3 } from '@/lib/bucket';
import { setServerAuthGuard } from '@/utils/auth';
import { buildError, sendError } from '@/utils/error';
import { FILE_NOT_FOUND_ERROR, FILE_TOO_LARGE_ERROR, INTERNAL_ERROR, INVALID_INPUT_ERROR, USER_NOT_FOUND_ERROR, WRONG_FILE_FORMAT_ERROR } from '@/utils/error/error-codes';
import { buildError, sendBuiltError } from '@/utils/error';
import { FILE_NOT_FOUND_ERROR, FILE_TOO_LARGE_ERROR, INVALID_INPUT_ERROR, USER_NOT_FOUND_ERROR, WRONG_FILE_FORMAT_ERROR } from '@/utils/error/error-codes';
import { AUTHORIZED_IMAGE_MIME_TYPES, AUTHORIZED_IMAGE_SIZE, convertFileRequestObjetToModel } from '@/utils/file.util';

export const GET = async () => {
Expand All @@ -16,34 +16,29 @@ export const GET = async () => {
const currentUserData = await findUserById(currentUser.id);

if (!currentUserData) {
return sendError(buildError({
throw buildError({
code: USER_NOT_FOUND_ERROR,
message: 'User not found.',
status: 404,
}));
});
}

const photoFileObject = currentUserData.photo_key ? await findFileByKey(currentUserData.photo_key) : null;
const photoFileObject = currentUserData.photo ? await findFileById(currentUserData.photo.id) : null;

if (!photoFileObject) {
return sendError(buildError({
throw buildError({
code: FILE_NOT_FOUND_ERROR,
message: 'File not found.',
status: 404,
}));
});
}

const photoUrl = await getFieldSignedURL(photoFileObject.key, 24 * 60 * 60);

return NextResponse.json({ photo_url: photoUrl });
} catch (error: any) {
console.error(error);
return sendError(buildError({
code: INTERNAL_ERROR,
message: error.message || 'An error occured.',
status: 500,
data: error,
}));
return sendBuiltError(error);
}
};

Expand All @@ -55,79 +50,73 @@ export const PUT = async (request: NextRequest) => {
const file = formData.get('avatar') as Blob | null;

if (!file) {
return sendError(buildError({
throw buildError({
code: INVALID_INPUT_ERROR,
message: 'No file provided.',
status: 422,
}));
});
}

if (!AUTHORIZED_IMAGE_MIME_TYPES.includes(file.type)) {
return sendError(buildError({
throw buildError({
code: WRONG_FILE_FORMAT_ERROR,
message: 'Wrong file format.',
status: 422,
}));
});
}

if (file.size > AUTHORIZED_IMAGE_SIZE) {
return sendError(buildError({
throw buildError({
code: FILE_TOO_LARGE_ERROR,
message: 'The file is too large.',
status: 422,
}));
});
}

const { user: currentUser } = await setServerAuthGuard();

const currentUserData = await findUserById(currentUser.id);

if (!currentUserData) {
return sendError(buildError({
throw buildError({
code: USER_NOT_FOUND_ERROR,
message: 'User not found.',
status: 404,
}));
});
}

if (currentUserData.photo_key && currentUserData.photo_key !== '') {
const oldFile = await findFileByKey(currentUserData.photo_key);
if (currentUserData.photo) {
const oldFile = await findFileById(currentUserData.photo.id);
if (oldFile) {
await deleteFileFromKey(oldFile.key);
await deleteFileById(oldFile.id);
}
}

const photoKey = await uploadImageToS3(file, 'avatars/');

const photoUrl = await getFieldSignedURL(photoKey, 24 * 60 * 60);


const parsedFile = {
...convertFileRequestObjetToModel(file, photoKey),
...convertFileRequestObjetToModel(file, {
key: photoKey,
url: photoUrl,
}),
created_by: currentUser.id,
};

const savedFile = await createFile(parsedFile);

const updatedCurrentUser = await updateUser({
id: currentUser.id,
photo_key: parsedFile.key,
photo: savedFile?.id,
updated_by: currentUser.id,
}, { newDocument: true });

const photoUrl = savedFile ? await getFieldSignedURL(savedFile.key, 24 * 60 * 60) : null;

if (updatedCurrentUser) {
updatedCurrentUser.photo_url = photoUrl;
}

return NextResponse.json(updatedCurrentUser);
} catch (error: any) {
console.error(error);
return sendError(buildError({
code: INTERNAL_ERROR,
message: error.message || 'An error occured.',
status: 500,
data: error,
}));
return sendBuiltError(error);
}
};
3 changes: 1 addition & 2 deletions app/api/auth/account/delete/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { FORBIDDEN_ERROR, INTERNAL_ERROR, PASSWORD_REQUIRED_ERROR, USER_NOT_FOUN
import { verifyPassword } from '@/utils/password.util';
import { USER_ACCOUNT_DELETION_SETTING } from '@/utils/settings';


export const POST = async (request: NextRequest) => {
try {

Expand Down Expand Up @@ -55,7 +54,7 @@ export const POST = async (request: NextRequest) => {
}));
}

const photoFileObject = userData.photo_key ? await findFileByKey(userData.photo_key) : null;
const photoFileObject = userData.photo ? await findFileByKey(userData.photo) : null;

if (photoFileObject) {
await deleteFileFromKey(photoFileObject.key);
Expand Down
Loading
0