tangled
alpha
login
or
join now
cosmik.network
/
semble
43
fork
atom
A social knowledge tool for researchers built on ATProto
43
fork
atom
overview
issues
13
pulls
pipelines
feat: public routes
Pouria Delfanazari
4 months ago
362372b8
9d1c118d
+178
-111
15 changed files
expand all
collapse all
unified
split
src
webapp
app
(auth)
login
page.tsx
(dashboard)
error.tsx
home
page.tsx
layout.tsx
api
auth
me
route.ts
components
navigation
dashboard
Dashboard.tsx
features
cards
lib
dal.ts
collections
lib
dal.ts
notes
lib
dal.ts
profile
components
profileMenu
ProfileMenu.tsx
lib
dal.ts
hooks
useAuth.tsx
lib
auth
dal.server.ts
dal.ts
token.ts
+5
-30
src/webapp/app/(auth)/login/page.tsx
···
14
Loader,
15
Badge,
16
} from '@mantine/core';
17
-
import { Suspense, useEffect, useState } from 'react';
18
import { IoMdHelpCircleOutline } from 'react-icons/io';
19
import SembleLogo from '@/assets/semble-logo.svg';
20
import { useAuth } from '@/hooks/useAuth';
···
22
import Link from 'next/link';
23
24
function InnerPage() {
25
-
const { isAuthenticated, isLoading } = useAuth();
26
-
const [isRedirecting, setIsRedirecting] = useState(false);
27
const router = useRouter();
28
const searchParams = useSearchParams();
29
const isExtensionLogin = searchParams.get('extension-login') === 'true';
30
31
useEffect(() => {
32
-
let timeoutId: NodeJS.Timeout;
33
-
34
if (isAuthenticated && !isExtensionLogin) {
35
-
setIsRedirecting(true);
36
-
37
-
// redirect after 1 second
38
-
timeoutId = setTimeout(() => {
39
-
router.push('/home');
40
-
}, 1000);
41
}
42
-
43
-
// clean up
44
-
return () => {
45
-
if (timeoutId) {
46
-
clearTimeout(timeoutId);
47
-
}
48
-
};
49
}, [isAuthenticated, router, isExtensionLogin]);
50
51
-
if (isLoading) {
52
return (
53
<Stack align="center">
54
-
<Loader type="dots" />
55
-
</Stack>
56
-
);
57
-
}
58
-
59
-
if (isRedirecting) {
60
-
return (
61
-
<Stack align="center">
62
-
<Text fw={500} fz={'xl'}>
63
-
Already logged in, redirecting you to library
64
-
</Text>
65
<Loader type="dots" />
66
</Stack>
67
);
···
14
Loader,
15
Badge,
16
} from '@mantine/core';
17
+
import { Suspense, useEffect } from 'react';
18
import { IoMdHelpCircleOutline } from 'react-icons/io';
19
import SembleLogo from '@/assets/semble-logo.svg';
20
import { useAuth } from '@/hooks/useAuth';
···
22
import Link from 'next/link';
23
24
function InnerPage() {
25
+
const { isAuthenticated, isLoading, refreshAuth } = useAuth();
0
26
const router = useRouter();
27
const searchParams = useSearchParams();
28
const isExtensionLogin = searchParams.get('extension-login') === 'true';
29
30
useEffect(() => {
0
0
31
if (isAuthenticated && !isExtensionLogin) {
32
+
refreshAuth();
33
+
router.push('/home');
0
0
0
0
34
}
0
0
0
0
0
0
0
35
}, [isAuthenticated, router, isExtensionLogin]);
36
37
+
if (isAuthenticated) {
38
return (
39
<Stack align="center">
0
0
0
0
0
0
0
0
0
0
0
40
<Loader type="dots" />
41
</Stack>
42
);
+73
src/webapp/app/(dashboard)/error.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
'use client';
2
+
3
+
import {
4
+
BackgroundImage,
5
+
Center,
6
+
Stack,
7
+
Image,
8
+
Badge,
9
+
Text,
10
+
Group,
11
+
Button,
12
+
Container,
13
+
} from '@mantine/core';
14
+
import SembleLogo from '@/assets/semble-logo.svg';
15
+
import BG from '@/assets/semble-bg.webp';
16
+
import Link from 'next/link';
17
+
import { BiRightArrowAlt } from 'react-icons/bi';
18
+
19
+
export default function Error() {
20
+
return (
21
+
<BackgroundImage
22
+
src={BG.src}
23
+
h={'100svh'}
24
+
pos={'fixed'}
25
+
top={0}
26
+
left={0}
27
+
style={{ zIndex: 102 }}
28
+
>
29
+
<Center h={'100svh'} py={{ base: '2rem', xs: '5rem' }}>
30
+
<Container size={'xl'} p={'md'} my={'auto'}>
31
+
<Stack>
32
+
<Stack align="center" gap={'xs'}>
33
+
<Image
34
+
src={SembleLogo.src}
35
+
alt="Semble logo"
36
+
w={48}
37
+
h={64.5}
38
+
mx={'auto'}
39
+
/>
40
+
<Badge size="sm">Alpha</Badge>
41
+
</Stack>
42
+
43
+
<Stack>
44
+
<Text fz={'h1'} fw={600} ta={'center'}>
45
+
A social knowledge network for researchers
46
+
</Text>
47
+
<Text fz={'h3'} fw={600} c={'#1F6144'} ta={'center'}>
48
+
Follow your peers’ research trails. Surface and discover new
49
+
connections. Built on ATProto so you own your data.
50
+
</Text>
51
+
</Stack>
52
+
53
+
<Group justify="center" gap="md" mt={'lg'}>
54
+
<Button component={Link} href="/signup" size="lg">
55
+
Sign up
56
+
</Button>
57
+
58
+
<Button
59
+
component={Link}
60
+
href="/login"
61
+
size="lg"
62
+
color="dark"
63
+
rightSection={<BiRightArrowAlt size={22} />}
64
+
>
65
+
Log in
66
+
</Button>
67
+
</Group>
68
+
</Stack>
69
+
</Container>
70
+
</Center>
71
+
</BackgroundImage>
72
+
);
73
+
}
+6
-1
src/webapp/app/(dashboard)/home/page.tsx
···
1
import HomeContainer from '@/features/home/containers/homeContainer/HomeContainer';
0
0
2
3
-
export default function Page() {
0
0
0
4
return <HomeContainer />;
5
}
···
1
import HomeContainer from '@/features/home/containers/homeContainer/HomeContainer';
2
+
import { verifySessionOnServer } from '@/lib/auth/dal.server';
3
+
import { redirect } from 'next/navigation';
4
5
+
export default async function Page() {
6
+
const session = await verifySessionOnServer();
7
+
if (!session) redirect('/login');
8
+
9
return <HomeContainer />;
10
}
-18
src/webapp/app/(dashboard)/layout.tsx
···
1
-
'use client';
2
-
3
-
import { useEffect } from 'react';
4
-
import { useRouter } from 'next/navigation';
5
-
import { useAuth } from '@/hooks/useAuth';
6
import Dashboard from '@/components/navigation/dashboard/Dashboard';
7
8
interface Props {
9
children: React.ReactNode;
10
}
11
export default function Layout(props: Props) {
12
-
const router = useRouter();
13
-
const { isAuthenticated, isLoading } = useAuth();
14
-
15
-
useEffect(() => {
16
-
if (!isLoading && !isAuthenticated) {
17
-
router.push('/login');
18
-
}
19
-
}, [isAuthenticated, isLoading, router]);
20
-
21
-
if (!isAuthenticated) {
22
-
return null; // Redirecting
23
-
}
24
-
25
return <Dashboard>{props.children}</Dashboard>;
26
}
···
0
0
0
0
0
1
import Dashboard from '@/components/navigation/dashboard/Dashboard';
2
3
interface Props {
4
children: React.ReactNode;
5
}
6
export default function Layout(props: Props) {
0
0
0
0
0
0
0
0
0
0
0
0
0
7
return <Dashboard>{props.children}</Dashboard>;
8
}
+25
-23
src/webapp/app/api/auth/me/route.ts
···
1
import { NextRequest, NextResponse } from 'next/server';
0
2
import { cookies } from 'next/headers';
3
import { isTokenExpiringSoon } from '@/lib/auth/token';
4
0
0
0
0
0
0
0
5
export async function GET(request: NextRequest) {
6
try {
7
const cookieStore = await cookies();
···
10
11
// No tokens at all - not authenticated
12
if (!accessToken && !refreshToken) {
13
-
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
14
}
15
16
// Check if accessToken is expired/missing or expiring soon (< 5 min)
17
if ((!accessToken || isTokenExpiringSoon(accessToken, 5)) && refreshToken) {
18
try {
19
// Proxy the refresh request completely to backend
20
-
const backendUrl =
21
-
process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000';
22
const refreshResponse = await fetch(
23
`${backendUrl}/api/users/oauth/refresh`,
24
{
···
32
);
33
34
if (!refreshResponse.ok) {
35
-
// Refresh failed - clear invalid tokens
36
-
const response = new NextResponse(
37
-
JSON.stringify({ error: 'Authentication failed' }),
38
{ status: 401 },
39
);
40
response.cookies.delete('accessToken');
41
response.cookies.delete('refreshToken');
0
42
return response;
43
}
44
···
56
});
57
58
if (!profileResponse.ok) {
59
-
return NextResponse.json(
60
-
{ error: 'Failed to fetch profile' },
61
-
{ status: profileResponse.status },
62
);
63
}
64
65
const user = await profileResponse.json();
66
67
// Return user profile with backend's Set-Cookie headers
68
-
return new Response(JSON.stringify({ user }), {
69
status: 200,
70
headers: {
71
'Content-Type': 'application/json',
72
'Set-Cookie': refreshResponse.headers.get('set-cookie') || '',
73
},
74
});
0
75
} catch (error) {
76
console.error('Token refresh error:', error);
77
-
return NextResponse.json(
78
-
{ error: 'Authentication failed' },
79
{ status: 500 },
80
);
81
}
···
94
});
95
96
if (!profileResponse.ok) {
97
-
return NextResponse.json(
98
-
{ error: 'Failed to fetch profile' },
99
{ status: profileResponse.status },
100
);
101
}
102
103
const user = await profileResponse.json();
104
-
return NextResponse.json({ user });
105
} catch (error) {
106
console.error('Profile fetch error:', error);
107
-
return NextResponse.json(
108
-
{ error: 'Failed to fetch profile' },
109
-
{ status: 500 },
110
-
);
111
}
112
} catch (error) {
113
console.error('Auth me error:', error);
114
-
return NextResponse.json(
115
-
{ error: 'Internal server error' },
116
-
{ status: 500 },
117
-
);
118
}
119
}
···
1
import { NextRequest, NextResponse } from 'next/server';
2
+
import type { GetProfileResponse } from '@/api-client/ApiClient';
3
import { cookies } from 'next/headers';
4
import { isTokenExpiringSoon } from '@/lib/auth/token';
5
6
+
const backendUrl = process.env.API_BASE_URL || 'http://127.0.0.1:3000';
7
+
8
+
type AuthResult = {
9
+
isAuth: boolean;
10
+
user?: GetProfileResponse;
11
+
};
12
+
13
export async function GET(request: NextRequest) {
14
try {
15
const cookieStore = await cookies();
···
18
19
// No tokens at all - not authenticated
20
if (!accessToken && !refreshToken) {
21
+
return NextResponse.json<AuthResult>({ isAuth: false }, { status: 401 });
22
}
23
24
// Check if accessToken is expired/missing or expiring soon (< 5 min)
25
if ((!accessToken || isTokenExpiringSoon(accessToken, 5)) && refreshToken) {
26
try {
27
// Proxy the refresh request completely to backend
0
0
28
const refreshResponse = await fetch(
29
`${backendUrl}/api/users/oauth/refresh`,
30
{
···
38
);
39
40
if (!refreshResponse.ok) {
41
+
// Refresh failed — clear tokens and mark as unauthenticated
42
+
const response = NextResponse.json<AuthResult>(
43
+
{ isAuth: false },
44
{ status: 401 },
45
);
46
response.cookies.delete('accessToken');
47
response.cookies.delete('refreshToken');
48
+
49
return response;
50
}
51
···
63
});
64
65
if (!profileResponse.ok) {
66
+
return NextResponse.json<AuthResult>(
67
+
{ isAuth: false },
68
+
{ status: 401 },
69
);
70
}
71
72
const user = await profileResponse.json();
73
74
// Return user profile with backend's Set-Cookie headers
75
+
const response = new Response(JSON.stringify({ isAuth: true, user }), {
76
status: 200,
77
headers: {
78
'Content-Type': 'application/json',
79
'Set-Cookie': refreshResponse.headers.get('set-cookie') || '',
80
},
81
});
82
+
return response;
83
} catch (error) {
84
console.error('Token refresh error:', error);
85
+
return NextResponse.json<AuthResult>(
86
+
{ isAuth: false },
87
{ status: 500 },
88
);
89
}
···
102
});
103
104
if (!profileResponse.ok) {
105
+
return NextResponse.json<AuthResult>(
106
+
{ isAuth: false },
107
{ status: profileResponse.status },
108
);
109
}
110
111
const user = await profileResponse.json();
112
+
return NextResponse.json<AuthResult>({ isAuth: true, user });
113
} catch (error) {
114
console.error('Profile fetch error:', error);
115
+
return NextResponse.json<AuthResult>({ isAuth: false }, { status: 500 });
0
0
0
116
}
117
} catch (error) {
118
console.error('Auth me error:', error);
119
+
return NextResponse.json<AuthResult>({ isAuth: false }, { status: 500 });
0
0
0
120
}
121
}
+11
src/webapp/components/navigation/dashboard/Dashboard.tsx
···
0
0
1
import AppLayout from '../appLayout/AppLayout';
0
0
2
3
interface Props {
4
children: React.ReactNode;
5
}
6
7
export default function Dashboard(props: Props) {
0
0
0
0
0
0
0
8
return <AppLayout>{props.children}</AppLayout>;
9
}
···
1
+
'use client';
2
+
3
import AppLayout from '../appLayout/AppLayout';
4
+
import GuestAppLayout from '../guestAppLayout/GuestAppLayout';
5
+
import { useAuth } from '@/hooks/useAuth';
6
7
interface Props {
8
children: React.ReactNode;
9
}
10
11
export default function Dashboard(props: Props) {
12
+
const { isAuthenticated, isLoading } = useAuth();
13
+
14
+
if (isLoading) return null;
15
+
16
+
if (!isAuthenticated)
17
+
return <GuestAppLayout>{props.children}</GuestAppLayout>;
18
+
19
return <AppLayout>{props.children}</AppLayout>;
20
}
+13
-6
src/webapp/features/cards/lib/dal.ts
···
0
1
import { createSembleClient } from '@/services/apiClient';
2
import { cache } from 'react';
3
···
14
});
15
16
export const getCardFromMyLibrary = cache(async (url: string) => {
17
-
// await verifySession();
0
18
const client = createSembleClient();
19
const response = await client.getUrlStatusForMyLibrary({ url: url });
20
···
22
});
23
24
export const getMyUrlCards = cache(async (params?: PageParams) => {
25
-
// await verifySession();
0
26
const client = createSembleClient();
27
const response = await client.getMyUrlCards({
28
page: params?.page,
···
37
url: string,
38
{ note, collectionIds }: { note?: string; collectionIds?: string[] },
39
) => {
40
-
// await verifySession();
0
41
const client = createSembleClient();
42
const response = await client.addUrlToLibrary({
43
url: url,
···
77
cardId: string;
78
collectionIds: string[];
79
}) => {
80
-
// await verifySession();
0
81
const client = createSembleClient();
82
const response = await client.removeCardFromCollection({
83
cardId,
···
89
);
90
91
export const removeCardFromLibrary = cache(async (cardId: string) => {
92
-
// await verifySession();
0
93
const client = createSembleClient();
94
const response = await client.removeCardFromLibrary({ cardId });
95
···
97
});
98
99
export const getLibrariesForCard = cache(async (cardId: string) => {
100
-
// await verifySession();
0
101
const client = createSembleClient();
102
const response = await client.getLibrariesForCard(cardId);
103
···
1
+
import { verifySessionOnClient } from '@/lib/auth/dal';
2
import { createSembleClient } from '@/services/apiClient';
3
import { cache } from 'react';
4
···
15
});
16
17
export const getCardFromMyLibrary = cache(async (url: string) => {
18
+
const session = await verifySessionOnClient();
19
+
if (!session) throw new Error('No session found');
20
const client = createSembleClient();
21
const response = await client.getUrlStatusForMyLibrary({ url: url });
22
···
24
});
25
26
export const getMyUrlCards = cache(async (params?: PageParams) => {
27
+
const session = await verifySessionOnClient();
28
+
if (!session) throw new Error('No session found');
29
const client = createSembleClient();
30
const response = await client.getMyUrlCards({
31
page: params?.page,
···
40
url: string,
41
{ note, collectionIds }: { note?: string; collectionIds?: string[] },
42
) => {
43
+
const session = await verifySessionOnClient();
44
+
if (!session) throw new Error('No session found');
45
const client = createSembleClient();
46
const response = await client.addUrlToLibrary({
47
url: url,
···
81
cardId: string;
82
collectionIds: string[];
83
}) => {
84
+
const session = await verifySessionOnClient();
85
+
if (!session) throw new Error('No session found');
86
const client = createSembleClient();
87
const response = await client.removeCardFromCollection({
88
cardId,
···
94
);
95
96
export const removeCardFromLibrary = cache(async (cardId: string) => {
97
+
const session = await verifySessionOnClient();
98
+
if (!session) throw new Error('No session found');
99
const client = createSembleClient();
100
const response = await client.removeCardFromLibrary({ cardId });
101
···
103
});
104
105
export const getLibrariesForCard = cache(async (cardId: string) => {
106
+
const session = await verifySessionOnClient();
107
+
if (!session) throw new Error('No session found');
108
const client = createSembleClient();
109
const response = await client.getLibrariesForCard(cardId);
110
+9
-5
src/webapp/features/collections/lib/dal.ts
···
0
1
import { createSembleClient } from '@/services/apiClient';
2
import { cache } from 'react';
3
···
40
41
export const getMyCollections = cache(
42
async (params?: PageParams & SearchParams) => {
43
-
// await verifySession();
44
-
45
const client = createSembleClient();
46
const response = await client.getMyCollections({
47
page: params?.page,
···
57
58
export const createCollection = cache(
59
async (newCollection: { name: string; description: string }) => {
60
-
// await verifySession();
0
61
const client = createSembleClient();
62
const response = await client.createCollection(newCollection);
63
···
66
);
67
68
export const deleteCollection = cache(async (id: string) => {
69
-
// await verifySession();
0
70
const client = createSembleClient();
71
const response = await client.deleteCollection({ collectionId: id });
72
···
80
name: string;
81
description?: string;
82
}) => {
83
-
// await verifySession();
0
84
const client = createSembleClient();
85
const response = await client.updateCollection(collection);
86
···
1
+
import { verifySessionOnClient } from '@/lib/auth/dal';
2
import { createSembleClient } from '@/services/apiClient';
3
import { cache } from 'react';
4
···
41
42
export const getMyCollections = cache(
43
async (params?: PageParams & SearchParams) => {
44
+
const session = await verifySessionOnClient();
45
+
if (!session) throw new Error('No session found');
46
const client = createSembleClient();
47
const response = await client.getMyCollections({
48
page: params?.page,
···
58
59
export const createCollection = cache(
60
async (newCollection: { name: string; description: string }) => {
61
+
const session = await verifySessionOnClient();
62
+
if (!session) throw new Error('No session found');
63
const client = createSembleClient();
64
const response = await client.createCollection(newCollection);
65
···
68
);
69
70
export const deleteCollection = cache(async (id: string) => {
71
+
const session = await verifySessionOnClient();
72
+
if (!session) throw new Error('No session found');
73
const client = createSembleClient();
74
const response = await client.deleteCollection({ collectionId: id });
75
···
83
name: string;
84
description?: string;
85
}) => {
86
+
const session = await verifySessionOnClient();
87
+
if (!session) throw new Error('No session found');
88
const client = createSembleClient();
89
const response = await client.updateCollection(collection);
90
+3
-1
src/webapp/features/notes/lib/dal.ts
···
0
1
import { createSembleClient } from '@/services/apiClient';
2
import { cache } from 'react';
3
···
21
22
export const updateNoteCard = cache(
23
async (note: { cardId: string; note: string }) => {
24
-
// await verifySession();
0
25
const client = createSembleClient();
26
const response = await client.updateNoteCard(note);
27
···
1
+
import { verifySessionOnClient } from '@/lib/auth/dal';
2
import { createSembleClient } from '@/services/apiClient';
3
import { cache } from 'react';
4
···
22
23
export const updateNoteCard = cache(
24
async (note: { cardId: string; note: string }) => {
25
+
const session = await verifySessionOnClient();
26
+
if (!session) throw new Error('No session found');
27
const client = createSembleClient();
28
const response = await client.updateNoteCard(note);
29
-2
src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx
···
4
Group,
5
Alert,
6
Menu,
7
-
Stack,
8
Image,
9
-
Text,
10
Button,
11
} from '@mantine/core';
12
import useMyProfile from '../../lib/queries/useMyProfile';
···
4
Group,
5
Alert,
6
Menu,
0
7
Image,
0
8
Button,
9
} from '@mantine/core';
10
import useMyProfile from '../../lib/queries/useMyProfile';
+3
-1
src/webapp/features/profile/lib/dal.ts
···
0
1
import { createSembleClient } from '@/services/apiClient';
2
import { cache } from 'react';
3
···
11
});
12
13
export const getMyProfile = cache(async () => {
14
-
// await verifySession();
0
15
const client = createSembleClient();
16
const response = await client.getMyProfile();
17
···
1
+
import { verifySessionOnClient } from '@/lib/auth/dal';
2
import { createSembleClient } from '@/services/apiClient';
3
import { cache } from 'react';
4
···
12
});
13
14
export const getMyProfile = cache(async () => {
15
+
const session = await verifySessionOnClient();
16
+
if (!session) throw new Error('No session found');
17
const client = createSembleClient();
18
const response = await client.getMyProfile();
19
+4
-15
src/webapp/hooks/useAuth.tsx
···
5
import { useRouter } from 'next/navigation';
6
import type { GetProfileResponse } from '@/api-client/ApiClient';
7
import { ClientCookieAuthService } from '@/services/auth/CookieAuthService.client';
8
-
9
-
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://127.0.0.1:4000';
10
11
interface AuthContextType {
12
user: GetProfileResponse | null;
···
28
29
const logout = async () => {
30
await ClientCookieAuthService.clearTokens();
31
-
queryClient.removeQueries({ queryKey: ['authenticated user'] });
32
router.push('/login');
33
};
34
35
const query = useQuery<GetProfileResponse | null>({
36
queryKey: ['authenticated user'],
37
queryFn: async () => {
38
-
const response = await fetch(`${appUrl}/api/auth/me`, {
39
-
method: 'GET',
40
-
credentials: 'include', // HttpOnly cookies sent automatically
41
-
});
42
-
// unauthenticated
43
-
if (!response.ok) {
44
-
throw new Error('Not authenticated');
45
-
}
46
-
47
-
const data = await response.json();
48
-
49
-
return data.user as GetProfileResponse;
50
},
51
staleTime: 5 * 60 * 1000, // cache for 5 minutes
52
refetchOnWindowFocus: false,
···
5
import { useRouter } from 'next/navigation';
6
import type { GetProfileResponse } from '@/api-client/ApiClient';
7
import { ClientCookieAuthService } from '@/services/auth/CookieAuthService.client';
8
+
import { verifySessionOnClient } from '@/lib/auth/dal';
0
9
10
interface AuthContextType {
11
user: GetProfileResponse | null;
···
27
28
const logout = async () => {
29
await ClientCookieAuthService.clearTokens();
30
+
queryClient.clear();
31
router.push('/login');
32
};
33
34
const query = useQuery<GetProfileResponse | null>({
35
queryKey: ['authenticated user'],
36
queryFn: async () => {
37
+
const session = await verifySessionOnClient();
38
+
return session;
0
0
0
0
0
0
0
0
0
0
39
},
40
staleTime: 5 * 60 * 1000, // cache for 5 minutes
41
refetchOnWindowFocus: false,
+20
src/webapp/lib/auth/dal.server.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { GetProfileResponse } from '@/api-client/ApiClient';
2
+
import { cookies } from 'next/headers';
3
+
import { cache } from 'react';
4
+
5
+
const appUrl = process.env.APP_URL || 'http://127.0.0.1:4000';
6
+
7
+
export const verifySessionOnServer = cache(async () => {
8
+
const cookieStore = await cookies();
9
+
const res = await fetch(`${appUrl}/api/auth/me`, {
10
+
headers: {
11
+
Cookie: cookieStore.toString(), // forward user's cookies
12
+
},
13
+
});
14
+
15
+
if (!res.ok) return null;
16
+
17
+
const { user }: { user: GetProfileResponse } = await res.json();
18
+
19
+
return user;
20
+
});
+5
-8
src/webapp/lib/auth/dal.ts
···
1
import type { GetProfileResponse } from '@/api-client/ApiClient';
2
-
import { ClientCookieAuthService } from '@/services/auth';
3
-
import { redirect } from 'next/navigation';
4
import { cache } from 'react';
5
6
-
const appUrl = process.env.APP_URL || 'http://127.0.0.1:4000';
7
8
-
export const verifySession = cache(
9
async (): Promise<GetProfileResponse | null> => {
10
const response = await fetch(`${appUrl}/api/auth/me`, {
11
method: 'GET',
···
13
});
14
15
if (!response.ok) {
16
-
await ClientCookieAuthService.clearTokens();
17
-
redirect('/login');
18
}
19
20
-
const data = await response.json();
21
22
-
return data.user as GetProfileResponse;
23
},
24
);
···
1
import type { GetProfileResponse } from '@/api-client/ApiClient';
0
0
2
import { cache } from 'react';
3
4
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://127.0.0.1:4000';
5
6
+
export const verifySessionOnClient = cache(
7
async (): Promise<GetProfileResponse | null> => {
8
const response = await fetch(`${appUrl}/api/auth/me`, {
9
method: 'GET',
···
11
});
12
13
if (!response.ok) {
14
+
return null;
0
15
}
16
17
+
const { user }: { user: GetProfileResponse } = await response.json();
18
19
+
return user;
20
},
21
);
+1
-1
src/webapp/lib/auth/token.ts
···
1
export const isTokenExpiringSoon = (
2
token: string | null | undefined,
3
-
bufferMinutes: number = 5,
4
): boolean => {
5
if (!token) return true;
6
···
1
export const isTokenExpiringSoon = (
2
token: string | null | undefined,
3
+
bufferMinutes: number = 5, // 5 minutes
4
): boolean => {
5
if (!token) return true;
6