Skip to content
Open
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
99 changes: 0 additions & 99 deletions src/pages/browse/[userId].tsx
Copy link
Member Author

@fetiu fetiu Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제거된 것으로 뜨지만, 아래 index.tsx 파일로 이름이 변경된 것. git mv 등을 사용해도 두 파일을 합치는 것이 불가능 한 듯...
추가 변경 내역은 다음 커밋 참조: 0ea6c77

Copy link
Member

@gomjellie gomjellie Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 diff 안만들고 먼저 파일명만 바꾸는 커밋 만든다음에 index.tsx 내용을 바꾸면됨

This file was deleted.

146 changes: 146 additions & 0 deletions src/pages/browse/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import type {
GetServerSideProps,
InferGetServerSidePropsType
} from 'next';
import { useState, useEffect, useCallback } from 'react';
import { api } from '~/utils/api';
import { formatDistanceToNow } from 'date-fns';
import styled from '@emotion/styled';
import Link from 'next/link';

const Container = styled('div')({
maxWidth: '1280px',
margin: '0 auto',
padding: '1rem',
});

const Title = styled('h1')({
fontSize: '1.5rem',
fontWeight: 'bold',
marginBottom: '1rem',
});

const Grid = styled('div')({
display: 'grid',
gap: '1rem',
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
gridTemplate: 'masonry',
});

const FileCard = styled('div')({
border: '1px solid #e5e7eb',
borderRadius: '0.5rem',
padding: '1rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center',
height: '400px',
width: '150px',
});

const BrowsePage = ({
login,
}: InferGetServerSidePropsType<typeof getServerSideProps>) => {
const [token, setToken] = useState<string | null>(null);
const utils = api.useContext();

const { data: currentUser } = api.account.getCurrentUser.useQuery(undefined, {
enabled: !!token,
});

const signInMutation = api.account.signIn.useMutation({
onSuccess: (data) => {
setToken(data.token);
localStorage.setItem('authToken', data.token);
void utils.s3.listUserFiles.invalidate();
},
});

const { data: filesData } = api.s3.listUserFiles.useQuery(
undefined,
{ enabled: !!token }
);

const handleLogin = useCallback(async () => {
const email = window.prompt('Email:');
if (!email) return;

const password = window.prompt('Password:');
if (!password) return;

void signInMutation.mutate({ email, password });
}, [signInMutation]);

// Check for stored token on mount
useEffect(() => {
const storedToken = localStorage.getItem('authToken');
if (storedToken && !login) {
setToken(storedToken);
} else if (!signInMutation.isSuccess && !signInMutation.isPending && !signInMutation.error) {
void handleLogin();
}
}, [currentUser, login, handleLogin, signInMutation]);

if (signInMutation.error) {
return (
<Container>
<Title>Access Denied</Title>
<div className="text-center text-red-500">
Login failed. Please refresh to try again.
</div>
</Container>
);
}

if (!token || !currentUser) {
return (
<Container>
<Title>Waiting for authentication...</Title>
<div className="text-center text-red-500">
Please refresh this page if nothing happens.
</div>
</Container>
);
}

return (
<Container>
<Title>My Files</Title>
<Grid>
{filesData?.files.map((file) => (
<Link href={file.downloadUrl} key={file.url}>
<FileCard>
<img
loading="lazy"
src={file.url}
alt="file"
style={{ width: '100%' }}
/>
<p className="text-sm text-gray-500">
{file.lastModified &&
formatDistanceToNow(new Date(file.lastModified))}{' '}
ago
</p>
</FileCard>
</Link>
))}
{filesData?.files.length === 0 && (
<div className="text-center text-gray-500">No files found</div>
)}
</Grid>
</Container>
);
};

export const getServerSideProps = (async ({ query }) => {
const login = query?.login ?? null;

return {
props: {
login,
},
};
}) satisfies GetServerSideProps;

export default BrowsePage;
12 changes: 7 additions & 5 deletions src/server/api/routers/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import { env } from '~/env.js';
import {
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from '~/server/api/trpc';
export const s3Router = createTRPCRouter({
listUserFiles: publicProcedure
listUserFiles: protectedProcedure
Copy link
Member Author

@fetiu fetiu Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 변경으로 authorization이 완전히 충족되는지는 모르겠으나, 이 엔드포인트를 이런 식으로 막아도 딱 원하는 동작까지는 달성할 수 있었고, https://4cut.us/download/[userId]/${filename}은 여전히 토큰 인증 없이도 동작함을 확인했습니다.

.meta({
openapi: {
method: 'GET',
Expand All @@ -19,7 +18,7 @@ export const s3Router = createTRPCRouter({
description: 'Returns list of files uploaded by the current user',
},
})
.input(z.object({ userId: z.string() }))
.input(z.void())
.output(
z.object({
files: z.array(
Expand All @@ -33,10 +32,13 @@ export const s3Router = createTRPCRouter({
),
})
)
.query(async ({ ctx, input: { userId } }) => {
.query(async ({ ctx }) => {
// Use the authenticated user's name from the session
const userName = ctx.session.user.name;

const response = await ctx.s3.listObjectsV2({
Bucket: env.BUCKET_NAME,
Prefix: `${userId}/`, // Only list objects that are in the user's folder
Prefix: `${userName}/`, // Only list objects that are in the user's folder
});

return {
Expand Down
5 changes: 5 additions & 0 deletions src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export const api = createTRPCNext<AppRouter>({
*/
transformer: superjson,
url: `${getBaseUrl()}/api/trpc`,
headers() {
// Add auth token to all requests
const token = localStorage.getItem("authToken");
return token ? { Authorization: `Bearer ${token}` } : {};
},
}),
],
};
Expand Down