Skip to content

Commit 7752f27

Browse files
committed
build create virtual lab components
1 parent a9ceac5 commit 7752f27

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+2276
-1872
lines changed

package-lock.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"elastic-builder": "^2.20.0",
7070
"file-saver": "^2.0.5",
7171
"github-markdown-css": "^5.8.1",
72+
"input-otp": "^1.4.2",
7273
"jotai": "^2.7.2",
7374
"jotai-devtools": "^0.10.0",
7475
"jotai-optics": "^0.3.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { getSession } from 'next-auth/react';
2+
3+
interface VerificationCodeEmailResponseData {
4+
verified_at?: Date | null;
5+
is_verified: boolean;
6+
remaining_attempts: number;
7+
remaining_time: number;
8+
locked: boolean;
9+
expired?: boolean;
10+
code_sent?: boolean | null;
11+
}
12+
13+
interface VerificationCodeEmailResponse {
14+
message: string;
15+
data: VerificationCodeEmailResponseData;
16+
}
17+
18+
export type VerificationCodeResponse<T extends 'init' | 'verify'> = T extends 'init'
19+
? {
20+
status: 'already_verified' | 'code_sent' | 'locked' | 'failed' | 'none';
21+
attempts?: number | null;
22+
wait?: number | null;
23+
// Other properties specific to Function A
24+
}
25+
: T extends 'verify'
26+
? {
27+
status: 'expired' | 'verified' | 'locked' | 'failed' | 'none';
28+
attempts?: number | null;
29+
wait?: number | null;
30+
// Other properties specific to Function B
31+
}
32+
: never;
33+
34+
type VerificationCodeInitPayload = {
35+
email: string;
36+
name: string;
37+
};
38+
type VerificationCodeVerifyPayload = VerificationCodeInitPayload & {
39+
code: number;
40+
};
41+
42+
export async function getEmailVerificationCode({
43+
email,
44+
name,
45+
}: VerificationCodeInitPayload): Promise<VerificationCodeResponse<'init'>> {
46+
try {
47+
const session = await getSession();
48+
const response = await fetch(`http://localhost:8000/virtual-labs/email/initiate-verification`, {
49+
method: 'post',
50+
headers: {
51+
'Content-Type': 'application/json',
52+
Authorization: `Bearer ${session?.accessToken}`,
53+
},
54+
body: JSON.stringify({
55+
email,
56+
virtual_lab_name: name,
57+
}),
58+
});
59+
if (response.ok) {
60+
const result = (await response.json()) as VerificationCodeEmailResponse;
61+
if (result.data.code_sent) {
62+
return {
63+
status: 'code_sent',
64+
attempts: result.data.remaining_attempts,
65+
wait: null,
66+
};
67+
}
68+
return { status: 'none' };
69+
}
70+
const result = (await response.json()) as VerificationCodeEmailResponse;
71+
return {
72+
// eslint-disable-next-line no-nested-ternary
73+
status: result.data.locked ? 'locked' : result.data.is_verified ? 'already_verified' : 'none',
74+
attempts: result.data.remaining_attempts,
75+
wait: result.data.remaining_time,
76+
};
77+
} catch (error) {
78+
return {
79+
status: 'failed',
80+
attempts: null,
81+
wait: null,
82+
};
83+
}
84+
}
85+
86+
export async function verifyOtpCode({
87+
email,
88+
name,
89+
code,
90+
}: VerificationCodeVerifyPayload): Promise<VerificationCodeResponse<'verify'>> {
91+
try {
92+
const session = await getSession();
93+
const response = await fetch(`http://localhost:8000/virtual-labs/email/verify-code`, {
94+
method: 'post',
95+
headers: {
96+
'Content-Type': 'application/json',
97+
Authorization: `Bearer ${session?.accessToken}`,
98+
},
99+
body: JSON.stringify({
100+
email,
101+
code,
102+
virtual_lab_name: name,
103+
}),
104+
});
105+
if (response.ok) {
106+
const result = (await response.json()) as VerificationCodeEmailResponse;
107+
108+
if (result.data.expired) {
109+
return {
110+
status: 'expired',
111+
};
112+
}
113+
if (result.data.is_verified) {
114+
return {
115+
status: 'verified',
116+
attempts: result.data.remaining_attempts,
117+
wait: null,
118+
};
119+
}
120+
return { status: 'none' };
121+
}
122+
const result = (await response.json()) as VerificationCodeEmailResponse;
123+
if (result.data.locked) {
124+
return {
125+
status: 'locked',
126+
attempts: result.data.remaining_attempts,
127+
wait: result.data.remaining_time,
128+
};
129+
}
130+
return {
131+
status: 'failed',
132+
attempts: result.data.remaining_attempts,
133+
wait: result.data.remaining_time,
134+
};
135+
} catch (error) {
136+
return {
137+
status: 'failed',
138+
attempts: null,
139+
wait: null,
140+
};
141+
}
142+
}
+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { getSession } from 'next-auth/react';
2+
3+
import { InviteResponse } from '@/api/virtual-lab-svc/queries/types';
4+
import { virtualLabApi } from '@/config';
5+
import { Role } from '@/api/virtual-lab-svc/types';
6+
7+
export async function inviteToProject({
8+
virtualLabId,
9+
projectId,
10+
email,
11+
role,
12+
}: {
13+
virtualLabId: string;
14+
projectId: string;
15+
email: string;
16+
role: Role;
17+
}): Promise<InviteResponse> {
18+
const session = await getSession();
19+
try {
20+
const response = await fetch(
21+
`${virtualLabApi.url}/virtual-labs/${virtualLabId}/projects/${projectId}/invites`,
22+
{
23+
method: 'post',
24+
headers: {
25+
'Content-Type': 'application/json',
26+
Authorization: `Bearer ${session?.accessToken}`,
27+
},
28+
body: JSON.stringify({
29+
email,
30+
role,
31+
}),
32+
}
33+
);
34+
35+
if (!response.ok) {
36+
const errorBody = await response.json();
37+
throw new Error(
38+
`Inviting ${email} as ${role} to project failed: ${response.status} - ${errorBody?.message || 'Unknown error'}`
39+
);
40+
}
41+
42+
const result: InviteResponse = await response.json();
43+
return result;
44+
} catch (error) {
45+
// TODO: capture exception with sentry
46+
// eslint-disable-next-line no-console
47+
console.error('Error inviting to project:', error);
48+
throw new Error(`Failed to invite user to project: ${(error as Error).message}`);
49+
}
50+
}
51+
52+
export async function inviteToVirtualLab({
53+
virtualLabId,
54+
email,
55+
role,
56+
}: {
57+
virtualLabId: string;
58+
email: string;
59+
role: Role;
60+
}): Promise<InviteResponse> {
61+
const session = await getSession();
62+
try {
63+
const response = await fetch(`${virtualLabApi.url}/virtual-labs/${virtualLabId}/invites`, {
64+
method: 'post',
65+
headers: {
66+
'Content-Type': 'application/json',
67+
Authorization: `Bearer ${session?.accessToken}`,
68+
},
69+
body: JSON.stringify({
70+
email,
71+
role,
72+
}),
73+
});
74+
75+
if (!response.ok) {
76+
const errorBody = await response.json();
77+
throw new Error(
78+
`Inviting ${email} as ${role} to virtual lab failed: ${response.status} - ${errorBody?.message || 'Unknown error'}`
79+
);
80+
}
81+
82+
const result: InviteResponse = await response.json();
83+
return result;
84+
} catch (error) {
85+
// TODO: capture exception with sentry
86+
// eslint-disable-next-line no-console
87+
console.error('Error inviting to project:', error);
88+
throw new Error(`Failed to invite user to project: ${(error as Error).message}`);
89+
}
90+
}
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { getSession } from 'next-auth/react';
2+
3+
import {
4+
ProjectCreationResponse,
5+
ProjectExistsVerificationResponse,
6+
} from '@/api/virtual-lab-svc/queries/types';
7+
import { ProjectPayload } from '@/api/virtual-lab-svc/types';
8+
import { virtualLabApi } from '@/config';
9+
10+
/**
11+
* Checks if a project with the given name already exists in a virtual lab.
12+
*
13+
* @param {string} vlabId - The ID of the virtual lab.
14+
* @param {string} name - The name of the project.
15+
* @returns {Promise<boolean>} - Returns `true` if the project exists, otherwise `false`.
16+
* @throws {Error} - Throws an error if the API request fails.
17+
*/
18+
export async function checkProjectExists({
19+
vlabId,
20+
name,
21+
}: {
22+
vlabId: string;
23+
name: string;
24+
}): Promise<boolean> {
25+
try {
26+
const session = await getSession();
27+
if (!session?.accessToken) {
28+
throw new Error('User session not found. Please log in.');
29+
}
30+
const response = await fetch(
31+
`${virtualLabApi.url}/virtual-labs/${vlabId}/projects/_check?q=${encodeURIComponent(name)}`,
32+
{
33+
method: 'get',
34+
headers: {
35+
'Content-Type': 'application/json',
36+
Authorization: `Bearer ${session.accessToken}`,
37+
},
38+
}
39+
);
40+
41+
if (!response.ok) {
42+
throw new Error('Validating project name failed');
43+
}
44+
45+
const result = (await response.json()) as ProjectExistsVerificationResponse;
46+
return result.data.exists;
47+
} catch (error) {
48+
// TODO: capture exception with sentry
49+
throw new Error(`Failed to check project existence: ${(error as Error).message}`);
50+
}
51+
}
52+
53+
export async function createProject(
54+
virtualLabId: string,
55+
{ name, description, include_members }: ProjectPayload
56+
): Promise<ProjectCreationResponse> {
57+
const session = await getSession();
58+
try {
59+
const response = await fetch(`${virtualLabApi.url}/virtual-labs/${virtualLabId}/projects`, {
60+
method: 'POST',
61+
headers: {
62+
'Content-Type': 'application/json',
63+
Authorization: `Bearer ${session?.accessToken}`,
64+
},
65+
body: JSON.stringify({
66+
name,
67+
description,
68+
include_members,
69+
}),
70+
});
71+
72+
if (!response.ok) {
73+
const errorBody = await response.json();
74+
throw new Error(
75+
`Creating project failed: ${response.status} - ${errorBody?.message || 'Unknown error'}`
76+
);
77+
}
78+
79+
const result: ProjectCreationResponse = await response.json();
80+
return result;
81+
} catch (error) {
82+
// TODO: capture exception with sentry
83+
// eslint-disable-next-line no-console
84+
console.error('Error creating project:', error);
85+
throw new Error(`Failed to create project: ${(error as Error).message}`);
86+
}
87+
}

0 commit comments

Comments
 (0)