Skip to content

Commit 28e1ef3

Browse files
authored
Merge pull request #69 from LabSyncro/feat/auth-with-hmi
Feat/auth with hmi
2 parents 2d85643 + 8998399 commit 28e1ef3

21 files changed

Lines changed: 959 additions & 124 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ GOOGLE_CLIENT_ID=
1111
GOOGLE_CLIENT_SECRET=
1212
PRINT_LABELS_ENDPOINT=
1313
AUTH_API_BASE_URL=
14+
CORS_ALLOWED_ORIGIN=
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<script setup lang="ts">
2+
import { laboratoryService } from "~/services";
3+
4+
const emit = defineEmits<{
5+
(
6+
e: "select",
7+
lab: { id: string; name: string; room: string; branch: string }
8+
): void;
9+
}>();
10+
11+
const isLoading = ref(true);
12+
const labs = ref<
13+
Array<{ id: string; name: string; room: string; branch: string }>
14+
>([]);
15+
const selectedLabId = ref("");
16+
const error = ref("");
17+
18+
onMounted(async () => {
19+
try {
20+
const response = await laboratoryService.getLabsManagedByAdmin(0, 500, {});
21+
labs.value = response.labs;
22+
} catch (err: any) {
23+
error.value = err.data?.message || "Failed to load laboratories";
24+
} finally {
25+
isLoading.value = false;
26+
}
27+
});
28+
29+
function handleSelect() {
30+
const selectedLab = labs.value.find((lab) => lab.id === selectedLabId.value);
31+
if (selectedLab) {
32+
emit("select", selectedLab);
33+
}
34+
}
35+
</script>
36+
37+
<template>
38+
<div class="space-y-4">
39+
<div v-if="isLoading" class="text-center py-4">
40+
<div
41+
class="inline-block w-6 h-6 border-2 border-tertiary-darker border-t-transparent rounded-full animate-spin"
42+
></div>
43+
<p class="mt-2 text-sm text-gray-500">Loading laboratories...</p>
44+
</div>
45+
46+
<div v-else-if="error" class="text-center text-red-600">
47+
{{ error }}
48+
</div>
49+
50+
<div v-else-if="labs.length === 0" class="text-center text-gray-500">
51+
No laboratories available.
52+
</div>
53+
54+
<div v-else class="space-y-4">
55+
<div class="space-y-2">
56+
<Select v-model="selectedLabId">
57+
<SelectTrigger id="lab-select" :disabled="isLoading">
58+
<SelectValue placeholder="Select a laboratory" />
59+
</SelectTrigger>
60+
<SelectContent>
61+
<div v-if="isLoading" class="flex items-center justify-center p-2">
62+
<div
63+
class="w-4 h-4 border-2 border-gray-300 border-t-primary rounded-full animate-spin"
64+
></div>
65+
</div>
66+
<SelectItem v-for="lab in labs" :key="lab.id" :value="lab.id">
67+
{{ lab.room }}, {{ lab.branch }}
68+
</SelectItem>
69+
</SelectContent>
70+
</Select>
71+
</div>
72+
73+
<Button
74+
:disabled="!selectedLabId"
75+
@click="handleSelect"
76+
class="w-full bg-tertiary-dark hover:bg-tertiary-darker"
77+
>
78+
Confirm Selection
79+
</Button>
80+
</div>
81+
</div>
82+
</template>
Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
<script setup lang="ts">
2-
const { signIn, data } = useAuth();
3-
const router = useRouter();
2+
const { data, signIn } = useAuth();
3+
const route = useRoute();
4+
5+
const hmiCode = computed(() => route.query.hmiCode as string | undefined);
6+
const defaultRoute = computed(() => data.value?.user?.defaultRoute || "/");
47
58
const handleLogin = async () => {
6-
await signIn('google');
7-
if (data.value?.user?.defaultRoute) {
8-
router.push(data.value.user.defaultRoute);
9+
let callbackUrl =
10+
(route.query.callbackUrl as string | undefined) || defaultRoute.value;
11+
12+
if (hmiCode.value && !route.query.callbackUrl) {
13+
callbackUrl = `/auth/hmi?hmiCode=${hmiCode.value}`;
14+
}
15+
16+
try {
17+
await signIn("google", {
18+
callbackUrl: callbackUrl,
19+
redirect: true,
20+
});
21+
} catch (err) {
22+
showError({ statusCode: 401, statusMessage: "Authentication failed" });
923
}
1024
};
1125
</script>
@@ -15,11 +29,11 @@ const handleLogin = async () => {
1529
@click="handleLogin"
1630
class="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-tertiary-darker transition-colors"
1731
>
18-
<img
19-
class="h-5 w-5"
20-
src="https://www.svgrepo.com/show/475656/google-color.svg"
32+
<img
33+
class="h-5 w-5"
34+
src="https://www.svgrepo.com/show/475656/google-color.svg"
2135
alt="Google logo"
22-
>
36+
/>
2337
<span>Sign in with Google</span>
2438
</button>
25-
</template>
39+
</template>

composables/useOneTimeQrCode.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import QRCode from 'qrcode';
22
import { useToast } from 'vue-toastification';
3+
import { authService } from '~/services/auth';
34

45
const toast = useToast();
56

@@ -160,9 +161,21 @@ export function useOneTimeQrCode() {
160161
/**
161162
* Handle a scanned QR code to verify the token inside
162163
* @param scannedQrData The data from the scanned QR code
163-
* @returns The user ID and any extra data if verification succeeds, null otherwise
164+
* @returns The user information and any extra data if verification succeeds, null otherwise
164165
*/
165-
const verifyScannedQrCode = async (scannedQrData: string): Promise<{ userId: string; extraData?: any } | null> => {
166+
const verifyScannedQrCode = async (scannedQrData: string): Promise<{
167+
userId: string;
168+
user: {
169+
id: string;
170+
name: string;
171+
email: string;
172+
tel?: string;
173+
avatar?: string;
174+
last_active_at?: string;
175+
roles: Array<{ name: string; key: string; }>;
176+
};
177+
extraData?: Record<string, any>;
178+
} | null> => {
166179
try {
167180
const qrData = JSON.parse(scannedQrData);
168181
const { token, userId, timestamp, expiry, ...extraData } = qrData;
@@ -174,11 +187,25 @@ export function useOneTimeQrCode() {
174187

175188
const isValid = await verifyToken(token, userId);
176189
if (!isValid) {
177-
toast.error("Mã QR không hợp lệ hoặc đã hết hạn");
190+
toast.error("Mã QR không hợp lệ");
178191
return null;
179192
}
180193

181-
return { userId, extraData };
194+
try {
195+
const result = await authService.verifyQrToken(token, userId, timestamp);
196+
return {
197+
userId,
198+
user: result.user,
199+
extraData: Object.keys(extraData).length > 0 ? extraData : undefined
200+
};
201+
} catch (error: any) {
202+
if (error.statusCode === 403) {
203+
toast.error("Mã QR đã được sử dụng");
204+
} else {
205+
toast.error("Lỗi khi xác thực mã QR");
206+
}
207+
return null;
208+
}
182209
} catch (error) {
183210
toast.error(`Lỗi khi xác thực mã QR: ${error}`);
184211
return null;

nuxt.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ export default defineNuxtConfig({
1919
},
2020
nitro: {
2121
preset: 'bun',
22+
routeRules: {
23+
'/api/**': {
24+
cors: true,
25+
headers: {
26+
'Access-Control-Allow-Origin': process.env.CORS_ALLOWED_ORIGIN || '',
27+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
28+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
29+
'Access-Control-Allow-Credentials': 'true'
30+
}
31+
}
32+
}
2233
},
2334
auth: {
2435
isEnabled: true,
@@ -75,6 +86,7 @@ export default defineNuxtConfig({
7586
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
7687
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
7788
AUTH_SECRET: process.env.AUTH_SECRET,
89+
CORS_ALLOWED_ORIGIN: process.env.CORS_ALLOWED_ORIGIN,
7890
},
7991
app: {
8092
head: {

pages/admin/borrows/form.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,6 @@ const handleVirtualKeyboardDetection = async (
250250
formState.userId = userId;
251251
await handleUserCodeChange(userId);
252252
toast.success("Xác thực người dùng thành công");
253-
} else {
254-
toast.error("Mã QR không hợp lệ hoặc đã hết hạn");
255253
}
256254
} catch (error) {
257255
toast.error("Không thể xác thực mã QR");

0 commit comments

Comments
 (0)