Skip to content

Commit ed677d0

Browse files
committed
feat: Add client IP handling across various routes and update API calls to include IP headers
1 parent 42de1fc commit ed677d0

29 files changed

Lines changed: 335 additions & 147 deletions

app/root.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,30 @@ export const meta: MetaFunction = () => {
4343
};
4444

4545
export const loader = async ({ request }: LoaderFunctionArgs) => {
46+
const url = new URL(request.url);
47+
48+
// Prevent infinite loop: if already on login/logout, don't check auth
49+
if (url.pathname === '/login' || url.pathname === '/logout') {
50+
return {
51+
token: null,
52+
user: null,
53+
isMobile: isMobileDetect({ ua: request.headers.get('user-agent') || '' }),
54+
allowedPlatforms: allowedLoginPlatforms,
55+
nullHeader: [
56+
{ t: 'board', r: 'routes/groups.$groupId.$categoryId.$boardId._index' },
57+
{ t: 'calendar', r: 'routes/groups.$groupId.calendar._index' },
58+
] as SidebarObject[],
59+
};
60+
}
61+
4662
let data: CachedResponse;
4763

4864
try {
4965
data = await getCachedUser(request);
5066
if (data?.data?.status === 401) throw new Error('Unauthorized.');
5167
} catch {
52-
return authenticator.logout(request, { redirectTo: '/' });
68+
// Redirect to logout which will clear session and redirect to login
69+
return authenticator.logout(request, { redirectTo: '/login' });
5370
}
5471

5572
return {
@@ -64,6 +81,11 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
6481
};
6582
};
6683

84+
// Prevent root loader from revalidating on every navigation
85+
export function shouldRevalidate() {
86+
return false;
87+
}
88+
6789
export default function App() {
6890
const { user, token, nullHeader, isMobile, allowedPlatforms } = useLoaderData<typeof loader>();
6991

app/routes/_index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
import { getIpHeaders, makeResponse } from '~/utils/functions.server';
12
import { LoaderFunctionArgs, redirect } from '@remix-run/node';
2-
import { makeResponse } from '~/utils/functions.server';
33
import { authenticator } from '~/utils/auth.server';
44
import { api } from '~/utils/web.server';
55

66
export const loader = async ({ request }: LoaderFunctionArgs) => {
77
const token = await authenticator.isAuthenticated(request);
88
if (!token) return redirect('/login');
99

10-
const DBGroups = await api?.groups.getGroups({ auth: token });
10+
const ipHeaders = getIpHeaders(request);
11+
if (!ipHeaders) throw makeResponse(null, 'Failed to get client IP.');
12+
13+
const DBGroups = await api?.groups.getGroups({ auth: token, headers: ipHeaders });
1114
if (!DBGroups || 'error' in DBGroups) throw makeResponse(DBGroups, 'Failed to get groups.');
1215

1316
const defaultGroup = DBGroups.data.find((group) => group.isDefault);

app/routes/admin.rooms._index.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { VStack, Box, Divider, Flex, Text, HStack, Avatar, AvatarGroup, Tooltip } from '@chakra-ui/react';
2-
import { makeResponse } from '~/utils/functions.server';
2+
import { getIpHeaders, makeResponse } from '~/utils/functions.server';
33
import { LoaderFunctionArgs } from '@remix-run/node';
44
import { IconLinkButton } from '~/components/Button';
55
import { authenticator } from '~/utils/auth.server';
@@ -27,10 +27,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
2727
const token = await authenticator.isAuthenticated(request);
2828
if (!token) throw makeResponse(null, 'You are not authorized to view this page.');
2929

30-
const DBRooms = await api?.admin.getActiveRooms({ auth: token });
30+
const ipHeaders = getIpHeaders(request);
31+
if (!ipHeaders) throw makeResponse(null, 'Failed to get client IP.');
32+
33+
const DBRooms = await api?.admin.getActiveRooms({ auth: token, headers: ipHeaders });
3134
if (!DBRooms || !('data' in DBRooms)) throw new Response(null, { status: 500, statusText: 'Failed to fetch rooms data.' });
3235

33-
const DBGroups = await api?.groups.getAllSorted({ auth: token });
36+
const DBGroups = await api?.groups.getAllSorted({ auth: token, headers: ipHeaders });
3437
if (!DBGroups || 'error' in DBGroups) throw makeResponse(DBGroups, 'Failed to get boards.');
3538

3639
const allBoards = DBGroups.data.flatMap((group) => group.categories.flatMap((category) => category.boards.map((board) => ({

app/routes/admin.users._index.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { VStack, Box, Flex, Text, Avatar, IconButton, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, Button, useColorMode, Badge, HStack, Divider, useToast, FormControl, FormLabel, Input, Tooltip } from '@chakra-ui/react';
22
import { FaClipboard, FaEye, FaFolder, FaLock, FaPen, FaQuestionCircle, FaTools, FaTrash, FaUnlock, FaUserPlus, FaUsers } from 'react-icons/fa';
3+
import { getIpHeaders, makeResObject, makeResponse, securityUtils } from '~/utils/functions.server';
34
import { getAll, GrantedEntry, PermUser, ResourceType } from '@excali-boards/boards-api-client';
4-
import { makeResObject, makeResponse, securityUtils } from '~/utils/functions.server';
55
import { FetcherWithComponents, useFetcher, useLoaderData } from '@remix-run/react';
66
import { firstToUpperCase, getGrantInfo, getRoleColor } from '~/other/utils';
77
import { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node';
@@ -20,20 +20,23 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
2020
if (!token) throw makeResponse(null, 'You are not authorized to view this page.');
2121
if (!api) throw makeResponse(null, 'API client not initialized.');
2222

23-
const DBUsers = await getAll((page, limit) => api!.admin.getUsers({ auth: token, page, limit }));
23+
const ipHeaders = getIpHeaders(request);
24+
if (!ipHeaders) throw makeResponse(null, 'Failed to get client IP.');
25+
26+
const DBUsers = await getAll((page, limit) => api!.admin.getUsers({ auth: token, page, limit, headers: ipHeaders }));
2427
if (!DBUsers || 'error' in DBUsers) throw makeResponse(DBUsers, 'Failed to get users.');
2528

2629
const userIds = DBUsers.data.data.map((user) => user.userId);
27-
const allPermissions = await api?.permissions.viewAllPermissions({ auth: token, userIds });
28-
if (!allPermissions || 'error' in allPermissions) throw makeResponse(allPermissions, 'Failed to get user permissions.');
30+
const DBAllPermissions = await api?.permissions.viewAllPermissions({ auth: token, userIds, headers: ipHeaders });
31+
if (!DBAllPermissions || 'error' in DBAllPermissions) throw makeResponse(DBAllPermissions, 'Failed to get user permissions.');
2932

3033
const findInviter = (invitedByUserId: string | null) => {
3134
if (!invitedByUserId) return null;
3235
return DBUsers.data.data.find((u) => u.userId === invitedByUserId) || null;
3336
};
3437

3538
return {
36-
userPermissions: allPermissions.data,
39+
userPermissions: DBAllPermissions.data,
3740
allUsers: DBUsers.data.data.map((user) => {
3841
const inviter = findInviter(user.invitedBy);
3942
return {
@@ -61,6 +64,9 @@ export const action = async ({ request }: ActionFunctionArgs) => {
6164
const formData = await request.formData();
6265
const type = formData.get('type') as string;
6366

67+
const ipHeaders = getIpHeaders(request);
68+
if (!ipHeaders) return makeResObject(null, 'Failed to get client IP.');
69+
6470
switch (type) {
6571
case 'revokePermission': {
6672
const userId = formData.get('userId') as string;
@@ -72,7 +78,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
7278
}
7379

7480
const result = await api?.permissions.revokePermissions({
75-
auth: token,
81+
auth: token, headers: ipHeaders,
7682
body: { userId, resourceType: resourceType as ResourceType, resourceId },
7783
});
7884

@@ -85,7 +91,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
8591
if (!userId || !newUsername) return { status: 400, error: 'Missing required fields.' };
8692

8793
const result = await api?.users.updateUser({
88-
auth: token, userId,
94+
auth: token, userId, headers: ipHeaders,
8995
body: { displayName: newUsername },
9096
});
9197

@@ -95,7 +101,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
95101
const userId = formData.get('userId') as string;
96102
if (!userId) return { status: 400, error: 'Invalid user id.' };
97103

98-
const result = await api?.users.deleteAccount({ auth: token, userId });
104+
const result = await api?.users.deleteAccount({ auth: token, userId, headers: ipHeaders });
99105
return makeResObject(result, 'Failed to delete user.');
100106
}
101107
default: {

app/routes/all._index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Flex, VStack, Accordion, AccordionItem, AccordionButton, AccordionPanel, Box, Text, Divider, AccordionIcon, Badge, useColorMode } from '@chakra-ui/react';
22
import { formatBytes, formatRelativeTime, getCardDeletionTime } from '~/other/utils';
3+
import { getIpHeaders, makeResponse } from '~/utils/functions.server';
34
import { Container } from '~/components/layout/Container';
4-
import { makeResponse } from '~/utils/functions.server';
55
import { LoaderFunctionArgs } from '@remix-run/node';
66
import { IconLinkButton } from '~/components/Button';
77
import { authenticator } from '~/utils/auth.server';
@@ -17,7 +17,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
1717
const token = await authenticator.isAuthenticated(request);
1818
if (!token) throw makeResponse(null, 'You are not authorized to view this page.');
1919

20-
const DBResources = await api?.groups.getAllSorted({ auth: token });
20+
const ipHeaders = getIpHeaders(request);
21+
if (!ipHeaders) throw makeResponse(null, 'Failed to get client IP.');
22+
23+
const DBResources = await api?.groups.getAllSorted({ auth: token, headers: ipHeaders });
2124
if (!DBResources || 'error' in DBResources) throw makeResponse(DBResources, 'Failed to get groups.');
2225

2326
return DBResources.data.map((group) => ({

app/routes/analytics.$groupId.$categoryId.$boardId._index.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { VStack, Box, Divider, Text, Table, Thead, Tbody, Tr, Th, Td, Avatar, Flex, useColorMode, useBreakpointValue } from '@chakra-ui/react';
22
import { formatRelativeTime, formatTime, time, validateParams } from '~/other/utils';
33
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
4+
import { getIpHeaders, makeResponse } from '~/utils/functions.server';
45
import { CustomTooltip } from '~/components/analytics/CustomTooltip';
56
import { StatGrid } from '~/components/analytics/StatGrid';
67
import { Container } from '~/components/layout/Container';
7-
import { makeResponse } from '~/utils/functions.server';
88
import { LoaderFunctionArgs } from '@remix-run/node';
99
import { authenticator } from '~/utils/auth.server';
1010
import MenuBar from '~/components/layout/MenuBar';
@@ -19,17 +19,20 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
1919
const token = await authenticator.isAuthenticated(request);
2020
if (!token) throw makeResponse(null, 'You are not authorized to view this page.');
2121

22-
const DBBoard = await api?.boards.getBoard({ auth: token, boardId, categoryId, groupId });
22+
const ipHeaders = getIpHeaders(request);
23+
if (!ipHeaders) throw makeResponse(null, 'Failed to get client IP.');
24+
25+
const DBBoard = await api?.boards.getBoard({ auth: token, boardId, categoryId, groupId, headers: ipHeaders });
2326
if (!DBBoard || 'error' in DBBoard) throw makeResponse(DBBoard, 'Failed to get board.');
2427

25-
const analytics = await api?.analytics.getBoardAnalytics({ auth: token, boardId, categoryId, groupId });
26-
if (!analytics || 'error' in analytics) throw makeResponse(analytics, 'Failed to get board analytics.');
28+
const DBAnalytics = await api?.analytics.getBoardAnalytics({ auth: token, boardId, categoryId, groupId, headers: ipHeaders });
29+
if (!DBAnalytics || 'error' in DBAnalytics) throw makeResponse(DBAnalytics, 'Failed to get board analytics.');
2730

2831
return {
2932
board: DBBoard.data.board,
3033
category: DBBoard.data.category,
3134
group: DBBoard.data.group,
32-
analytics: analytics.data,
35+
analytics: DBAnalytics.data,
3336
};
3437
};
3538

app/routes/analytics.$groupId.$categoryId._index.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { VStack, Box, Divider, Text, Table, Thead, Tbody, Tr, Th, Td, Avatar, Flex, useColorMode, useBreakpointValue } from '@chakra-ui/react';
22
import { formatRelativeTime, formatTime, validateParams } from '~/other/utils';
33
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
4+
import { getIpHeaders, makeResponse } from '~/utils/functions.server';
45
import { CustomTooltip } from '~/components/analytics/CustomTooltip';
56
import { StatGrid } from '~/components/analytics/StatGrid';
67
import { Container } from '~/components/layout/Container';
7-
import { makeResponse } from '~/utils/functions.server';
88
import { BoardMapType, MapType } from '~/other/types';
99
import { LoaderFunctionArgs } from '@remix-run/node';
1010
import { authenticator } from '~/utils/auth.server';
@@ -20,19 +20,22 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
2020
const token = await authenticator.isAuthenticated(request);
2121
if (!token) throw makeResponse(null, 'You are not authorized to view this page.');
2222

23-
const DBGroup = await api?.groups.getGroup({ auth: token, groupId });
23+
const ipHeaders = getIpHeaders(request);
24+
if (!ipHeaders) throw makeResponse(null, 'Failed to get client IP.');
25+
26+
const DBGroup = await api?.groups.getGroup({ auth: token, groupId, headers: ipHeaders });
2427
if (!DBGroup || 'error' in DBGroup) throw makeResponse(DBGroup, 'Failed to get group.');
2528

26-
const category = DBGroup.data.categories.find(c => c.id === categoryId);
29+
const category = DBGroup.data.categories.find((c) => c.id === categoryId);
2730
if (!category) throw makeResponse(null, 'Category not found.');
2831

29-
const analytics = await api?.analytics.getCategoryAnalytics({ auth: token, categoryId, groupId });
30-
if (!analytics || 'error' in analytics) throw makeResponse(analytics, 'Failed to get category analytics.');
32+
const DBAnalytics = await api?.analytics.getCategoryAnalytics({ auth: token, categoryId, groupId, headers: ipHeaders });
33+
if (!DBAnalytics || 'error' in DBAnalytics) throw makeResponse(DBAnalytics, 'Failed to get category analytics.');
3134

3235
return {
3336
group: DBGroup.data.group,
3437
category,
35-
analytics: analytics.data,
38+
analytics: DBAnalytics.data,
3639
};
3740
};
3841

app/routes/analytics.$groupId._index.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { VStack, Box, Divider, Text, Table, Thead, Tbody, Tr, Th, Td, Avatar, Flex, useColorMode, useBreakpointValue } from '@chakra-ui/react';
22
import { formatRelativeTime, formatTime, time, validateParams } from '~/other/utils';
33
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
4+
import { getIpHeaders, makeResponse } from '~/utils/functions.server';
45
import { CustomTooltip } from '~/components/analytics/CustomTooltip';
56
import { StatGrid } from '~/components/analytics/StatGrid';
67
import { Container } from '~/components/layout/Container';
7-
import { makeResponse } from '~/utils/functions.server';
88
import { BoardMapType, MapType } from '~/other/types';
99
import { LoaderFunctionArgs } from '@remix-run/node';
1010
import { authenticator } from '~/utils/auth.server';
@@ -20,15 +20,18 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
2020
const token = await authenticator.isAuthenticated(request);
2121
if (!token) throw makeResponse(null, 'You are not authorized to view this page.');
2222

23-
const DBGroup = await api?.groups.getGroup({ auth: token, groupId });
23+
const ipHeaders = getIpHeaders(request);
24+
if (!ipHeaders) throw makeResponse(null, 'Failed to get client IP.');
25+
26+
const DBGroup = await api?.groups.getGroup({ auth: token, groupId, headers: ipHeaders });
2427
if (!DBGroup || 'error' in DBGroup) throw makeResponse(DBGroup, 'Failed to get group.');
2528

26-
const analytics = await api?.analytics.getGroupAnalytics({ auth: token, groupId });
27-
if (!analytics || 'error' in analytics) throw makeResponse(analytics, 'Failed to get group analytics.');
29+
const DBAnalytics = await api?.analytics.getGroupAnalytics({ auth: token, groupId, headers: ipHeaders });
30+
if (!DBAnalytics || 'error' in DBAnalytics) throw makeResponse(DBAnalytics, 'Failed to get group analytics.');
2831

2932
return {
3033
group: DBGroup.data.group,
31-
analytics: analytics.data,
34+
analytics: DBAnalytics.data,
3235
};
3336
};
3437

app/routes/analytics._index.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { VStack, Box, Divider, Text, Table, Thead, Tbody, Tr, Th, Td, Flex, useColorMode, useBreakpointValue } from '@chakra-ui/react';
22
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
3+
import { getIpHeaders, makeResponse } from '~/utils/functions.server';
34
import { CustomTooltip } from '~/components/analytics/CustomTooltip';
45
import { StatGrid } from '~/components/analytics/StatGrid';
56
import { Container } from '~/components/layout/Container';
6-
import { makeResponse } from '~/utils/functions.server';
77
import { LoaderFunctionArgs } from '@remix-run/node';
88
import { authenticator } from '~/utils/auth.server';
99
import MenuBar from '~/components/layout/MenuBar';
@@ -16,10 +16,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
1616
const token = await authenticator.isAuthenticated(request);
1717
if (!token) throw makeResponse(null, 'You are not authorized to view this page.');
1818

19-
const analytics = await api?.analytics.getUserAnalytics({ auth: token });
20-
if (!analytics || 'error' in analytics) throw makeResponse(analytics, 'Failed to get analytics.');
19+
const ipHeaders = getIpHeaders(request);
20+
if (!ipHeaders) throw makeResponse(null, 'Failed to get client IP.');
2121

22-
return { analytics: analytics.data };
22+
const DBAnalytics = await api?.analytics.getUserAnalytics({ auth: token, headers: ipHeaders });
23+
if (!DBAnalytics || 'error' in DBAnalytics) throw makeResponse(DBAnalytics, 'Failed to get analytics.');
24+
25+
return { analytics: DBAnalytics.data };
2326
};
2427

2528
export default function UserAnalytics() {

app/routes/flashcards.$groupId.$categoryId.$boardId._index.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { Box, BoxProps, Button, Flex, HStack, IconButton, Slider, SliderFilledTr
22
import { FaArrowLeft, FaArrowRight, FaBookOpen, FaCog, FaList, FaRandom } from 'react-icons/fa';
33
import { ActionFunctionArgs, LinkDescriptor, LoaderFunctionArgs } from '@remix-run/node';
44
import { useState, useMemo, useCallback, useRef, useEffect, useContext } from 'react';
5+
import { getIpHeaders, makeResObject, makeResponse } from '~/utils/functions.server';
56
import { themeColor, themeColorLight, WebReturnType } from '~/other/types';
6-
import { makeResObject, makeResponse } from '~/utils/functions.server';
77
import { IconLinkButton, LinkButton } from '~/components/Button';
88
import { ConfettiContainer } from '~/components/other/Confetti';
99
import { useFetcher, useLoaderData } from '@remix-run/react';
@@ -26,10 +26,13 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
2626
const token = await authenticator.isAuthenticated(request);
2727
if (!token) throw makeResponse(null, 'You are not authorized to view this page.');
2828

29-
const flashcardData = await api?.flashcards.getDeck({ auth: token, groupId, categoryId, boardId });
30-
if (!flashcardData || 'error' in flashcardData) throw makeResponse(flashcardData, 'Failed to get flashcard deck.');
29+
const ipHeaders = getIpHeaders(request);
30+
if (!ipHeaders) throw makeResponse(null, 'Failed to get client IP.');
3131

32-
return { deck: flashcardData.data, groupId, categoryId, boardId };
32+
const DBFlashcardData = await api?.flashcards.getDeck({ auth: token, groupId, categoryId, boardId, headers: ipHeaders });
33+
if (!DBFlashcardData || 'error' in DBFlashcardData) throw makeResponse(DBFlashcardData, 'Failed to get flashcard deck.');
34+
35+
return { deck: DBFlashcardData.data, groupId, categoryId, boardId };
3336
};
3437

3538
export const action = async ({ request, params }: ActionFunctionArgs) => {
@@ -38,6 +41,9 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
3841
const token = await authenticator.isAuthenticated(request);
3942
if (!token) throw makeResponse(null, 'You are not authorized to view this page.');
4043

44+
const ipHeaders = getIpHeaders(request);
45+
if (!ipHeaders) return makeResObject(null, 'Failed to get client IP.');
46+
4147
const formData = await request.formData();
4248
const type = formData.get('type') as string;
4349

@@ -52,6 +58,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
5258
categoryId,
5359
boardId,
5460
body: { currentIndex, completed },
61+
headers: ipHeaders,
5562
});
5663

5764
return makeResObject(result, 'Failed to update progress.');

0 commit comments

Comments
 (0)