250715
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -67,3 +67,4 @@ node_modules/
|
|||||||
|
|
||||||
# dataconnect generated files
|
# dataconnect generated files
|
||||||
.dataconnect
|
.dataconnect
|
||||||
|
sendgrid.env
|
||||||
|
|||||||
@@ -383,6 +383,8 @@ import { syncBoardAndUploadsData } from '@/utils/boardUtils';
|
|||||||
import { useUserStore } from '@/stores/user';
|
import { useUserStore } from '@/stores/user';
|
||||||
//types
|
//types
|
||||||
import { UploadSettings } from '@/data/config';
|
import { UploadSettings } from '@/data/config';
|
||||||
|
import { sendBoardEmail } from '@/utils/api/sendBoardEmail';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
WadizBoard,
|
WadizBoard,
|
||||||
BoardItem,
|
BoardItem,
|
||||||
@@ -405,10 +407,12 @@ const userId = computed(() => userStore.docId);
|
|||||||
const { $firebase } = useNuxtApp();
|
const { $firebase } = useNuxtApp();
|
||||||
const wadizesCollection = $firebase.wadizesCollection;
|
const wadizesCollection = $firebase.wadizesCollection;
|
||||||
const currentCollection = wadizesCollection;
|
const currentCollection = wadizesCollection;
|
||||||
const currentBoard = 'wadiz';
|
const currentBoard = 'wadizes';
|
||||||
const compData = {
|
const compData = {
|
||||||
title: '공지사항 | NOTICE',
|
title: 'WADIZES | 예약 신청',
|
||||||
};
|
};
|
||||||
|
import { generateWadizUploadedEmail } from '@/utils/emailTemplates/wadizUploaded';
|
||||||
|
|
||||||
//loading Message
|
//loading Message
|
||||||
const loadingMessage = 'Uploading! 잠시만 기다려주세요...';
|
const loadingMessage = 'Uploading! 잠시만 기다려주세요...';
|
||||||
const isUploading = ref(false);
|
const isUploading = ref(false);
|
||||||
@@ -541,6 +545,19 @@ const handleUpload = async () => {
|
|||||||
})) as WadizBoard;
|
})) as WadizBoard;
|
||||||
|
|
||||||
await handleUpdateBoard(boardPayload, currentCollection);
|
await handleUpdateBoard(boardPayload, currentCollection);
|
||||||
|
//sendemail
|
||||||
|
try {
|
||||||
|
const { subject, html } = generateWadizUploadedEmail(boardPayload);
|
||||||
|
await sendBoardEmail({
|
||||||
|
access: 'public',
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
action: 'created',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Notification email failed', e);
|
||||||
|
// optional: toast but don’t abort the UX
|
||||||
|
}
|
||||||
showAlert('성공적으로 수정되었습니다', 'bg-green-800');
|
showAlert('성공적으로 수정되었습니다', 'bg-green-800');
|
||||||
emit('success');
|
emit('success');
|
||||||
} else {
|
} else {
|
||||||
@@ -571,7 +588,22 @@ const handleUpload = async () => {
|
|||||||
'yyyy-MM-dd'
|
'yyyy-MM-dd'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
//sendEmail
|
||||||
|
try {
|
||||||
|
const { subject, html } = generateWadizUploadedEmail(boardPayload);
|
||||||
|
|
||||||
|
await sendBoardEmail({
|
||||||
|
access: 'public',
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
action: 'created',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Notification email failed', e);
|
||||||
|
// optional: toast but don’t abort the UX
|
||||||
|
}
|
||||||
await handleCreateBoard(boardPayload, currentCollection);
|
await handleCreateBoard(boardPayload, currentCollection);
|
||||||
|
|
||||||
showAlert('성공적으로 생성되었습니다', 'bg-green-800', true);
|
showAlert('성공적으로 생성되었습니다', 'bg-green-800', true);
|
||||||
emit('success');
|
emit('success');
|
||||||
}
|
}
|
||||||
|
|||||||
156
bobu/app/components/boards/wadiz/WadizListSingle.vue
Normal file
156
bobu/app/components/boards/wadiz/WadizListSingle.vue
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<li
|
||||||
|
class="py-3 transition duration-300 hover:bg-gray-50 dark:hover:bg-gray-800 items-start border-b border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col mx-4 justify-center md:flex-row text-normal">
|
||||||
|
<!-- Announcement icon or board number -->
|
||||||
|
<div
|
||||||
|
v-if="!showSelectBox"
|
||||||
|
class="pr-4 md:pr-8 text-gray-400 hidden md:block"
|
||||||
|
>
|
||||||
|
<template v-if="item.announcement">
|
||||||
|
<font-awesome-icon :icon="iconName" fade style="color: #990000" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ item.boards_number }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selection checkbox -->
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
v-if="showSelectBox"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="selected"
|
||||||
|
@change="selectItem"
|
||||||
|
class="mr-8 inline-block text-gray-600 dark:text-white"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Title link -->
|
||||||
|
<NuxtLink
|
||||||
|
:to="`${routeName}/${item.docId}`"
|
||||||
|
class="cursor-pointer flex-1 pr-8 inline-block text-gray-600 dark:text-white hover:underline"
|
||||||
|
>
|
||||||
|
{{ truncateTitle(item.name) }}
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- Display author only for Manager+ -->
|
||||||
|
<div
|
||||||
|
v-if="userRole >= ROLE_THRESHOLD.MANAGER"
|
||||||
|
class="text-xs text-gray-400 flex items-center pr-4"
|
||||||
|
>
|
||||||
|
{{ userDisplayName }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date -->
|
||||||
|
<div class="inline-block text-gray-400 text-left md:hidden">
|
||||||
|
{{ formatDate(item.created) }}
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:inline-block text-gray-400 text-left">
|
||||||
|
{{ formatDate2(item.created) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue';
|
||||||
|
import { fetchUserDisplayName } from '@/utils/boardUtils';
|
||||||
|
import { useUserStore } from '@/stores/user';
|
||||||
|
import { ROLE_THRESHOLD } from '@/data/config';
|
||||||
|
import type { WadizBoard, BoardItem } from '@/types';
|
||||||
|
|
||||||
|
// Helper to normalize Firestore timestamp or other formats into JS Date
|
||||||
|
function toDate(raw: unknown): Date {
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
return new Date(raw);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
raw &&
|
||||||
|
typeof raw === 'object' &&
|
||||||
|
'toDate' in (raw as any) &&
|
||||||
|
typeof (raw as any).toDate === 'function'
|
||||||
|
) {
|
||||||
|
return (raw as any).toDate();
|
||||||
|
}
|
||||||
|
const sec = (raw as any)?.seconds ?? (raw as any)?._seconds;
|
||||||
|
if (typeof sec === 'number') {
|
||||||
|
return new Date(sec * 1000);
|
||||||
|
}
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps<{
|
||||||
|
item: WadizBoard;
|
||||||
|
showSelectBox?: boolean;
|
||||||
|
selected?: boolean;
|
||||||
|
iconName?: [string, string];
|
||||||
|
routeName: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'select', item: WadizBoard): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Reactive state
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const userRole = computed(() => userStore.userRole);
|
||||||
|
const userDisplayName = ref('');
|
||||||
|
|
||||||
|
// Title truncation length
|
||||||
|
const truncatedLength = ref(6);
|
||||||
|
|
||||||
|
// Date formatting
|
||||||
|
const formatDate = (raw: unknown) => {
|
||||||
|
const d = toDate(raw);
|
||||||
|
const YYYY = d.getFullYear();
|
||||||
|
const MM = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const DD = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${YYYY}-${MM}-${DD}`;
|
||||||
|
};
|
||||||
|
const formatDate2 = (raw: unknown) => {
|
||||||
|
const d = toDate(raw);
|
||||||
|
const MM = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const DD = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${MM}-${DD}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Title truncation based on screen width
|
||||||
|
const updateTruncatedLength = () => {
|
||||||
|
const w = window.innerWidth / 14;
|
||||||
|
truncatedLength.value = Math.round(Math.min(w, 60));
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
updateTruncatedLength();
|
||||||
|
window.addEventListener('resize', updateTruncatedLength);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateTruncatedLength);
|
||||||
|
});
|
||||||
|
const truncateTitle = (t: string) =>
|
||||||
|
t.length > truncatedLength.value
|
||||||
|
? t.slice(0, truncatedLength.value) + '…'
|
||||||
|
: t;
|
||||||
|
|
||||||
|
// Fetch user display name for manager+
|
||||||
|
watch(
|
||||||
|
() => props.item.userId,
|
||||||
|
async (uid) => {
|
||||||
|
if (uid && userRole.value >= ROLE_THRESHOLD.MANAGER) {
|
||||||
|
userDisplayName.value = await fetchUserDisplayName(uid);
|
||||||
|
} else {
|
||||||
|
userDisplayName.value = uid || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Selection emit
|
||||||
|
const selectItem = () => {
|
||||||
|
emit('select', props.item);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -21,7 +21,7 @@ export function useBoardList<T extends BoardItem>(
|
|||||||
title = 'Board',
|
title = 'Board',
|
||||||
itemsPerPage = 20,
|
itemsPerPage = 20,
|
||||||
defaultSort = 'desc',
|
defaultSort = 'desc',
|
||||||
access = 'public',
|
access,
|
||||||
loadingMessage = '잠시만 기다려주세요...',
|
loadingMessage = '잠시만 기다려주세요...',
|
||||||
} = options;
|
} = options;
|
||||||
/* UI + meta */
|
/* UI + meta */
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<AdminLogin />
|
<div v-if="userLoggedIn">
|
||||||
|
<button @click="userStore.signOut">로그아웃</button>
|
||||||
|
<NuxtLink to="/wadiz/manage">TO manage</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<AdminLogin />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AdminLogin from "~/components/auth/admin-login.vue";
|
import AdminLogin from '~/components/auth/admin-login.vue';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const userLoggedIn = computed(() => userStore.userLoggedIn);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (userLoggedIn.value) {
|
||||||
|
navigateTo('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
447
bobu/app/pages/wadiz/[docId].vue
Normal file
447
bobu/app/pages/wadiz/[docId].vue
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mx-auto max-w-5xl w-full py-4 px-6 items-center">
|
||||||
|
<div class="border-b border-gray-200 dark:border-gray-700 pb-8">
|
||||||
|
<div class="mx-auto mt-10 max-w-lg space-y-8">
|
||||||
|
<!-- 1) 와디즈 결제 번호 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="paymentId"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
와디즈 결제 번호
|
||||||
|
</label>
|
||||||
|
<VeeField
|
||||||
|
name="paymentId"
|
||||||
|
id="paymentId"
|
||||||
|
type="text"
|
||||||
|
rules="required"
|
||||||
|
v-model="board.paymentId"
|
||||||
|
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
|
||||||
|
/>
|
||||||
|
<VeeErrorMessage name="paymentId" class="text-red-500 text-sm mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2) 성함 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="name"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
성함
|
||||||
|
</label>
|
||||||
|
<VeeField
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
rules="required"
|
||||||
|
v-model="board.name"
|
||||||
|
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
|
||||||
|
/>
|
||||||
|
<VeeErrorMessage name="name" class="text-red-500 text-sm mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3) 주소 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="address"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
주소
|
||||||
|
</label>
|
||||||
|
<VeeField
|
||||||
|
name="address"
|
||||||
|
id="address"
|
||||||
|
type="text"
|
||||||
|
rules="required"
|
||||||
|
v-model="board.address"
|
||||||
|
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600"
|
||||||
|
/>
|
||||||
|
<VeeErrorMessage name="address" class="text-red-500 text-sm mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4) 메일 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="email"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
메일
|
||||||
|
</label>
|
||||||
|
<VeeField
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
rules="required|email"
|
||||||
|
v-model="board.email"
|
||||||
|
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
|
||||||
|
/>
|
||||||
|
<VeeErrorMessage name="email" class="text-red-500 text-sm mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5) 본인 연락처 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="phone"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
본인 연락처 (Your Phone)
|
||||||
|
</label>
|
||||||
|
<VeeField
|
||||||
|
name="phone"
|
||||||
|
id="phone"
|
||||||
|
type="tel"
|
||||||
|
rules="required"
|
||||||
|
v-model="board.phone"
|
||||||
|
placeholder="010-1234-5678"
|
||||||
|
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
|
||||||
|
/>
|
||||||
|
<VeeErrorMessage name="phone" class="text-red-500 text-sm mt-1" />
|
||||||
|
</div>
|
||||||
|
<!-- 6) 긴급 연락처 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="emergencyPhone"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
긴급 연락처 | 본인 외 제3자
|
||||||
|
</label>
|
||||||
|
<VeeField
|
||||||
|
name="emergencyPhone"
|
||||||
|
id="emergencyPhone"
|
||||||
|
type="tel"
|
||||||
|
rules="required"
|
||||||
|
v-model="board.emergencyPhone"
|
||||||
|
placeholder="010-0000-0000"
|
||||||
|
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600"
|
||||||
|
/>
|
||||||
|
<VeeErrorMessage
|
||||||
|
name="emergencyPhone"
|
||||||
|
class="text-red-500 text-sm mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 7) 예약 일정 선택 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
예약 일정 선택
|
||||||
|
</label>
|
||||||
|
<div class="mt-2 flex gap-4">
|
||||||
|
<!-- 시작일 -->
|
||||||
|
<div class="w-1/2">
|
||||||
|
<label
|
||||||
|
for="scheduleStart"
|
||||||
|
class="mb-1 block text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
시작일
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 종료일 -->
|
||||||
|
<div class="w-1/2">
|
||||||
|
<label
|
||||||
|
for="scheduleEnd"
|
||||||
|
class="mb-1 block text-sm text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
종료일
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="board.scheduleEnd"
|
||||||
|
id="scheduleEnd"
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
placeholder="자동 계산 (1박 2일)"
|
||||||
|
class="w-full rounded-md bg-gray-100 dark:bg-gray-700 px-3 py-2 text-gray-400 dark:text-gray-400 border dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
예약불가 날짜: 2025년 10월 3일(금)~5일(일), 연박 불가 / 체류가능일 :
|
||||||
|
금·토·일
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 8) 본인 참석 여부 -->
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
본인 참석 여부 (Are you attending?)
|
||||||
|
</span>
|
||||||
|
<div class="mt-2 flex items-center gap-6">
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<VeeField
|
||||||
|
name="attending"
|
||||||
|
type="radio"
|
||||||
|
:value="'yes'"
|
||||||
|
v-model="board.attending"
|
||||||
|
class="h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
<span class="ml-2 text-gray-700 dark:text-gray-300"
|
||||||
|
>예 (Yes)</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<VeeField
|
||||||
|
name="attending"
|
||||||
|
type="radio"
|
||||||
|
:value="'no'"
|
||||||
|
v-model="board.attending"
|
||||||
|
class="h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
<span class="ml-2 text-gray-700 dark:text-gray-300"
|
||||||
|
>아니오 (No)</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<VeeErrorMessage name="attending" class="text-red-500 text-sm mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 8-2) If "아니오", show alternate attendee fields -->
|
||||||
|
<div v-if="board.attending === 'no'" class="space-y-4">
|
||||||
|
<!-- 대리 참석자 성함 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="altName"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
대리 참석자 성함 (Alternate Attendee Name)
|
||||||
|
</label>
|
||||||
|
<VeeField
|
||||||
|
name="altName"
|
||||||
|
id="altName"
|
||||||
|
type="text"
|
||||||
|
v-model="board.altName"
|
||||||
|
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
|
||||||
|
/>
|
||||||
|
<VeeErrorMessage name="altName" class="text-red-500 text-sm mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 대리 참석자 연락처 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="altPhone"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
대리 참석자 연락처 (Alternate Attendee Phone)
|
||||||
|
</label>
|
||||||
|
<VeeField
|
||||||
|
name="altPhone"
|
||||||
|
id="altPhone"
|
||||||
|
type="tel"
|
||||||
|
v-model="board.altPhone"
|
||||||
|
placeholder="010-1234-5678"
|
||||||
|
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
|
||||||
|
/>
|
||||||
|
<VeeErrorMessage
|
||||||
|
name="altPhone"
|
||||||
|
class="text-red-500 text-sm mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 9) 참여 동반자 정보 -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="companions"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
참여 동반자 정보 (선택)
|
||||||
|
</label>
|
||||||
|
<VeeField
|
||||||
|
name="companions"
|
||||||
|
id="companions"
|
||||||
|
type="text"
|
||||||
|
v-model="board.companions"
|
||||||
|
placeholder="동반자 성함 등"
|
||||||
|
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
<VeeErrorMessage
|
||||||
|
name="companions"
|
||||||
|
class="text-red-500 text-sm mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 10) 알레르기나 건강 특이사항 (선택) -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="healthNotes"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
알레르기나 건강 특이사항 (선택)
|
||||||
|
</label>
|
||||||
|
<VeeField
|
||||||
|
name="healthNotes"
|
||||||
|
id="healthNotes"
|
||||||
|
v-model="board.healthNotes"
|
||||||
|
as="textarea"
|
||||||
|
rows="2"
|
||||||
|
placeholder="예: 견과류 알레르기"
|
||||||
|
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
<VeeErrorMessage
|
||||||
|
name="healthNotes"
|
||||||
|
class="text-red-500 text-sm mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 11) 전달하고 싶은 말 (선택) -->
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="remarks"
|
||||||
|
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
전달하고 싶은 말 (선택)
|
||||||
|
</label>
|
||||||
|
<VeeField
|
||||||
|
name="remarks"
|
||||||
|
id="remarks"
|
||||||
|
v-model="board.remarks"
|
||||||
|
as="textarea"
|
||||||
|
rows="4"
|
||||||
|
placeholder="예: 채팅으로 연락해주세요."
|
||||||
|
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
<VeeErrorMessage name="remarks" class="text-red-500 text-sm mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
|
import useUserStore from '@/stores/user';
|
||||||
|
import AppBoardHeader from '@/components/boards/BoardHeader.vue';
|
||||||
|
import AppBoardAction from '@/components/boards/BoardAction.vue';
|
||||||
|
import AppBoardBody from '@/components/boards/BoardBody.vue';
|
||||||
|
import AppLoadingOverlay from '@/components/LoadingOverlay.vue';
|
||||||
|
import { loadBoardDetails, deleteSingle } from '@/utils/boardUtils';
|
||||||
|
import type {
|
||||||
|
BoardItem,
|
||||||
|
OrderByDirection,
|
||||||
|
WadizBoard,
|
||||||
|
FileItem,
|
||||||
|
} from '@/types';
|
||||||
|
const { $firebase } = useNuxtApp();
|
||||||
|
const storage = $firebase.storage;
|
||||||
|
|
||||||
|
//***Variables, Things you need to change****)
|
||||||
|
const wadizesCollection = $firebase.wadizesCollection;
|
||||||
|
import AppUploadWadizForm from '@/components/boards/wadiz/UploadWadizForm.vue';
|
||||||
|
import AppUploadWadiz from '@/pages/wadiz/upload.vue';
|
||||||
|
const currentCollection = wadizesCollection;
|
||||||
|
const currentBoardRouteName = '/wadiz';
|
||||||
|
const compData = {
|
||||||
|
title: '와디즈 | WADIZ',
|
||||||
|
routeName: '/wadiz/[docId]', // path to single notice view
|
||||||
|
itemsPerPage: 20,
|
||||||
|
defaultSort: 'desc' as OrderByDirection,
|
||||||
|
listRouteName: '/wadiz',
|
||||||
|
uploadRouteName: '/wadiz/upload',
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
const in_submission = ref(false);
|
||||||
|
const deletingMessage = '삭제 중! 잠시만 기다려주세요...';
|
||||||
|
|
||||||
|
//Reactive variables
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute(); // Access the route object
|
||||||
|
const docId = computed(() => route.params.docId as string);
|
||||||
|
|
||||||
|
const board: Ref<WadizBoard> = ref({
|
||||||
|
docId: '',
|
||||||
|
userId: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
boardState: { state: 'processing' },
|
||||||
|
announcement: false,
|
||||||
|
created: '',
|
||||||
|
files: [],
|
||||||
|
//depreciated
|
||||||
|
boards_number: 0,
|
||||||
|
thumbnail: { name: '', url: '' } as FileItem,
|
||||||
|
ishidden: false,
|
||||||
|
//wadizes
|
||||||
|
name: '',
|
||||||
|
paymentId: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
emergencyPhone: '',
|
||||||
|
scheduleStart: '',
|
||||||
|
scheduleEnd: '',
|
||||||
|
attending: 'yes',
|
||||||
|
altName: '',
|
||||||
|
altPhone: '',
|
||||||
|
companions: '',
|
||||||
|
healthNotes: '',
|
||||||
|
remarks: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const userRole = computed(() => userStore.userRole);
|
||||||
|
|
||||||
|
const toggleEdit = ref(false);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const result = await deleteSingle(board.value, currentCollection, storage);
|
||||||
|
if (result) {
|
||||||
|
router.push(currentBoardRouteName); // path string works directly in Nuxt3
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateSuccess = async () => {
|
||||||
|
await loadBoardDetails(
|
||||||
|
board.value.docId,
|
||||||
|
board, // ref<BoardItem>
|
||||||
|
currentCollection,
|
||||||
|
router,
|
||||||
|
currentBoardRouteName,
|
||||||
|
userRole // Ref<number>
|
||||||
|
);
|
||||||
|
|
||||||
|
toggleEdit.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEditBoard = () => {
|
||||||
|
toggleEdit.value = !toggleEdit.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(docId, (newId) => {
|
||||||
|
if (!newId) return;
|
||||||
|
console.log('newId', newId);
|
||||||
|
loadBoardDetails(
|
||||||
|
newId,
|
||||||
|
board,
|
||||||
|
currentCollection,
|
||||||
|
router,
|
||||||
|
currentBoardRouteName,
|
||||||
|
userRole
|
||||||
|
);
|
||||||
|
});
|
||||||
|
onMounted(async () => {
|
||||||
|
if (docId.value) {
|
||||||
|
await loadBoardDetails(
|
||||||
|
docId.value,
|
||||||
|
board,
|
||||||
|
currentCollection,
|
||||||
|
router,
|
||||||
|
currentBoardRouteName,
|
||||||
|
userRole
|
||||||
|
);
|
||||||
|
// Anything here will run AFTER board.value has real data
|
||||||
|
console.log('after await:', board.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.board {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
101
bobu/app/pages/wadiz/manage.vue
Normal file
101
bobu/app/pages/wadiz/manage.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<section>
|
||||||
|
<app-boards-header
|
||||||
|
:h2data="compdata.title"
|
||||||
|
v-model="selectedSort"
|
||||||
|
:sortOptions="sortOptions"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<div class="text-center">
|
||||||
|
<p v-if="!userLoggedIn">You are not logged in</p>
|
||||||
|
<p v-else>You are logged in for {{ userStore.userRole }}</p>
|
||||||
|
<button @click="verifySession">verifySession</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-board-list
|
||||||
|
v-model="selectedSort"
|
||||||
|
:title="compdata.title"
|
||||||
|
:sortOptions="sortOptions"
|
||||||
|
:userRole="userRole"
|
||||||
|
:current-page="currentPage"
|
||||||
|
:total-pages="totalPages"
|
||||||
|
:page-numbers="pageNumbers"
|
||||||
|
:show-select-boxes="showSelectBoxes"
|
||||||
|
:uploadRoute="currentUploadRoute"
|
||||||
|
@toggle-select-boxes="showSelectBoxes = !showSelectBoxes"
|
||||||
|
@delete-selected="onDeleteSelected"
|
||||||
|
@go-to-page="onGoToPage"
|
||||||
|
@prev-page="onPrevPage"
|
||||||
|
@next-page="onNextPage"
|
||||||
|
:isLoading="isLoading"
|
||||||
|
:loadingMessage="loadingMessage"
|
||||||
|
>
|
||||||
|
<template #list>
|
||||||
|
<app-board-list-single
|
||||||
|
v-for="item in currentItems"
|
||||||
|
:key="item.docId"
|
||||||
|
:userId="item.userId"
|
||||||
|
:item="item"
|
||||||
|
:showSelectBox="showSelectBoxes"
|
||||||
|
@select="onToggleSelect(item)"
|
||||||
|
:iconName="['fas', 'bell']"
|
||||||
|
:routeName="currentBoardRouteName"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</app-board-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AppBoardsHeader from '@/components/boards/BoardHeader.vue';
|
||||||
|
import AppBoardList from '@/components/boards/BoardList.vue';
|
||||||
|
import AppBoardListSingle from '@/components/boards/wadiz/WadizListSingle.vue';
|
||||||
|
import type { OrderByDirection, BoardAccessMode, WadizBoard } from '@/types';
|
||||||
|
import { useUserStore } from '@/stores/user';
|
||||||
|
import { verifySession } from '@/utils/api/verifyFromFunction';
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const userLoggedIn = computed(() => userStore.userLoggedIn);
|
||||||
|
|
||||||
|
//customize
|
||||||
|
const access: BoardAccessMode = 'public';
|
||||||
|
const currentCollection = 'wadizes';
|
||||||
|
const currentUploadRoute = '/wadiz/upload';
|
||||||
|
const currentBoardRouteName = '/wadiz';
|
||||||
|
const compData = {
|
||||||
|
title: '와디즈 관리',
|
||||||
|
itemsPerPage: 20,
|
||||||
|
defaultSort: 'desc' as OrderByDirection,
|
||||||
|
};
|
||||||
|
const loading = '게시물을 불러오는 중입니다...';
|
||||||
|
import { useBoardList } from '@/composables/useBoardList';
|
||||||
|
|
||||||
|
// destructure only the things you actually need
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
loadingMessage,
|
||||||
|
compdata,
|
||||||
|
currentItems,
|
||||||
|
currentPage,
|
||||||
|
pageNumbers,
|
||||||
|
selectedSort,
|
||||||
|
sortOptions,
|
||||||
|
userRole,
|
||||||
|
totalPages,
|
||||||
|
selectedItems,
|
||||||
|
showSelectBoxes,
|
||||||
|
onToggleSelect,
|
||||||
|
onDeleteSelected,
|
||||||
|
onGoToPage,
|
||||||
|
onPrevPage,
|
||||||
|
onNextPage,
|
||||||
|
} = useBoardList<WadizBoard>(currentCollection, {
|
||||||
|
title: compData.title,
|
||||||
|
itemsPerPage: compData.itemsPerPage,
|
||||||
|
defaultSort: compData.defaultSort,
|
||||||
|
access: access,
|
||||||
|
loadingMessage: loading,
|
||||||
|
});
|
||||||
|
|
||||||
|
// no more fetchBoardsAndUpdateItems or onBeforeMount
|
||||||
|
</script>
|
||||||
@@ -75,6 +75,7 @@ export const useUserStore = defineStore('user', {
|
|||||||
everLoggedIn = true;
|
everLoggedIn = true;
|
||||||
const idToken = await user.getIdToken();
|
const idToken = await user.getIdToken();
|
||||||
await createSession(idToken);
|
await createSession(idToken);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for 100 milliseconds
|
||||||
this.userLoggedIn = true;
|
this.userLoggedIn = true;
|
||||||
this.initializeListener();
|
this.initializeListener();
|
||||||
} else if (everLoggedIn) {
|
} else if (everLoggedIn) {
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { useRuntimeConfig } from '#imports';
|
import { useRuntimeConfig } from '#imports';
|
||||||
import type {
|
import type { BoardItem, BoardAccessMode, CursorResponse } from '~/types';
|
||||||
BoardItem,
|
|
||||||
BoardAccessMode,
|
|
||||||
CursorResponse,
|
|
||||||
UseBoardListOptions,
|
|
||||||
} from '~/types';
|
|
||||||
|
|
||||||
/* ---------- params accepted by this helper --------------------- */
|
|
||||||
interface FetchBoardsParams {
|
interface FetchBoardsParams {
|
||||||
sortOrder?: 'asc' | 'desc';
|
sortOrder?: 'asc' | 'desc';
|
||||||
pageNumber?: number;
|
pageNumber?: number;
|
||||||
@@ -15,9 +9,11 @@ interface FetchBoardsParams {
|
|||||||
pageToken?: string;
|
pageToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FUNCTION_BASE = 'https://fetchboards-edvvp3hbnq-du.a.run.app';
|
// ⛳️ Split base URLs per function (if deployed separately)
|
||||||
|
const BASE_FETCH_BOARDS = 'https://fetchboards-edvvp3hbnq-du.a.run.app';
|
||||||
|
const BASE_FETCH_BOARDS_CURSOR =
|
||||||
|
'https://fetchboardscursor-edvvp3hbnq-du.a.run.app';
|
||||||
|
|
||||||
/* --------------------------------------------------------------- */
|
|
||||||
export async function fetchBoardsFromFunction<T extends BoardItem>(
|
export async function fetchBoardsFromFunction<T extends BoardItem>(
|
||||||
collection: string,
|
collection: string,
|
||||||
{
|
{
|
||||||
@@ -29,7 +25,7 @@ export async function fetchBoardsFromFunction<T extends BoardItem>(
|
|||||||
}: FetchBoardsParams = {}
|
}: FetchBoardsParams = {}
|
||||||
): Promise<CursorResponse<T>> {
|
): Promise<CursorResponse<T>> {
|
||||||
const isCursorMode = !!pageToken || pageNumber === undefined;
|
const isCursorMode = !!pageToken || pageNumber === undefined;
|
||||||
const endpoint = isCursorMode ? 'fetchBoardsCursor' : 'fetchBoards';
|
const endpoint = isCursorMode ? BASE_FETCH_BOARDS_CURSOR : BASE_FETCH_BOARDS;
|
||||||
|
|
||||||
const params: Record<string, any> = {
|
const params: Record<string, any> = {
|
||||||
collection,
|
collection,
|
||||||
@@ -39,9 +35,11 @@ export async function fetchBoardsFromFunction<T extends BoardItem>(
|
|||||||
...(isCursorMode ? { pageToken } : { pageNumber }),
|
...(isCursorMode ? { pageToken } : { pageNumber }),
|
||||||
};
|
};
|
||||||
|
|
||||||
return await $fetch<CursorResponse<T>>(`${FUNCTION_BASE}/${endpoint}`, {
|
const result = await $fetch(endpoint, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params,
|
params,
|
||||||
credentials: access !== 'public' ? 'include' : undefined,
|
credentials: access !== 'public' ? 'include' : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return result as CursorResponse<T>;
|
||||||
}
|
}
|
||||||
|
|||||||
33
bobu/app/utils/api/fetchSingleItemFromFunction.ts
Normal file
33
bobu/app/utils/api/fetchSingleItemFromFunction.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useRuntimeConfig } from '#imports';
|
||||||
|
import type { BoardItem, BoardAccessMode } from '~/types';
|
||||||
|
|
||||||
|
interface FetchSingleItemParams {
|
||||||
|
collection: string;
|
||||||
|
docId: string;
|
||||||
|
access?: BoardAccessMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FUNCTION_BASE = 'https://fetchsingleitem-edvvp3hbnq-du.a.run.app';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single board item from Firebase HTTPS function
|
||||||
|
*/
|
||||||
|
export async function fetchSingleItemFromFunction<T extends BoardItem>({
|
||||||
|
collection,
|
||||||
|
docId,
|
||||||
|
access = 'public',
|
||||||
|
}: FetchSingleItemParams): Promise<T> {
|
||||||
|
const params = {
|
||||||
|
collection,
|
||||||
|
docId,
|
||||||
|
access,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await $fetch(`${FUNCTION_BASE}`, {
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
credentials: access !== 'public' ? 'include' : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result as T;
|
||||||
|
}
|
||||||
24
bobu/app/utils/api/sendBoardEmail.ts
Normal file
24
bobu/app/utils/api/sendBoardEmail.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// utils/api/sendBoardMail.ts
|
||||||
|
import type { BoardAccessMode } from '~/types';
|
||||||
|
|
||||||
|
interface SendBoardMailParams {
|
||||||
|
access?: BoardAccessMode;
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
action?: 'created' | 'updated' | 'deleted' | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FUNCTION_BASE = 'https://boardmail-edvvp3hbnq-du.a.run.app';
|
||||||
|
|
||||||
|
export async function sendBoardEmail({
|
||||||
|
access = 'public',
|
||||||
|
action = 'updated',
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
}: SendBoardMailParams): Promise<void> {
|
||||||
|
await $fetch(FUNCTION_BASE, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { access, subject, html, action },
|
||||||
|
credentials: access !== 'public' ? 'include' : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// utils/api/verifyFromFunction.ts
|
||||||
|
|
||||||
const VERIFY_URL = 'https://verifysession-edvvp3hbnq-du.a.run.app';
|
const VERIFY_URL = 'https://verifysession-edvvp3hbnq-du.a.run.app';
|
||||||
|
|
||||||
export type VerifiedSession = {
|
export type VerifiedSession = {
|
||||||
@@ -5,9 +7,39 @@ export type VerifiedSession = {
|
|||||||
email: string;
|
email: string;
|
||||||
role: number;
|
role: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function verifySession(): Promise<VerifiedSession> {
|
export async function verifySession(): Promise<VerifiedSession> {
|
||||||
return await $fetch<VerifiedSession>(VERIFY_URL, {
|
console.log('verifySession (client): Initiating GET request to', VERIFY_URL); // Log start
|
||||||
method: 'GET',
|
|
||||||
credentials: 'include', // ensures session cookie is sent
|
try {
|
||||||
});
|
// We use $fetchRaw here to inspect the full response, including headers
|
||||||
|
const response = await $fetch.raw<VerifiedSession>(VERIFY_URL, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include', // ensures session cookie is sent
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('verifySession (client): Response status:', response.status);
|
||||||
|
console.log('verifySession (client): Response headers:', response.headers); // Log all response headers
|
||||||
|
|
||||||
|
// To check if a specific cookie was sent by the browser in the *request* (not response):
|
||||||
|
// This is hard to do directly with $fetch on the client-side *before* the request is sent,
|
||||||
|
// you'd typically use the Network tab for that.
|
||||||
|
|
||||||
|
// If you need the response body:
|
||||||
|
const data = await response._data;
|
||||||
|
console.log('verifySession (client): Response data:', data);
|
||||||
|
|
||||||
|
if (response.status === 200 && data) {
|
||||||
|
console.log('verifySession (client): Session verification successful.');
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
const errorMessage = `verifySession (client): Verification failed with status ${response.status}`;
|
||||||
|
console.error(errorMessage);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('verifySession (client): Error during verification:', error);
|
||||||
|
// Re-throw the error so it can be caught by the calling code
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export const loadBoardDetails = async (
|
|||||||
router.push({ name: `${currentboard}list` });
|
router.push({ name: `${currentboard}list` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log('board loaded', board.value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading board details:', error);
|
console.error('Error loading board details:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
88
bobu/app/utils/emailTemplates/wadizUploaded.ts
Normal file
88
bobu/app/utils/emailTemplates/wadizUploaded.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// utils/emailTemplates/wadizUploaded.ts
|
||||||
|
|
||||||
|
import type { WadizBoard } from '~/types/boardItem';
|
||||||
|
|
||||||
|
export function generateWadizUploadedEmail(board: Partial<WadizBoard>) {
|
||||||
|
const subject = `${board.name || '신청자'}님의 예약 신청이 접수되었습니다`;
|
||||||
|
const html = `
|
||||||
|
<h2>예약 정보</h2>
|
||||||
|
<table>
|
||||||
|
${
|
||||||
|
board.name
|
||||||
|
? `<tr><td><strong>이름</strong></td><td>${board.name}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.paymentId
|
||||||
|
? `<tr><td><strong>결제 ID</strong></td><td>${board.paymentId}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.email
|
||||||
|
? `<tr><td><strong>이메일</strong></td><td>${board.email}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.phone
|
||||||
|
? `<tr><td><strong>전화번호</strong></td><td>${board.phone}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.address
|
||||||
|
? `<tr><td><strong>주소</strong></td><td>${board.address}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.emergencyPhone
|
||||||
|
? `<tr><td><strong>비상연락처</strong></td><td>${board.emergencyPhone}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.scheduleStart
|
||||||
|
? `<tr><td><strong>시작일</strong></td><td>${board.scheduleStart}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.scheduleEnd
|
||||||
|
? `<tr><td><strong>종료일</strong></td><td>${board.scheduleEnd}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.attending
|
||||||
|
? `<tr><td><strong>참석 여부</strong></td><td>${
|
||||||
|
board.attending === 'yes' ? '참석' : '불참'
|
||||||
|
}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.altName
|
||||||
|
? `<tr><td><strong>대리인 이름</strong></td><td>${board.altName}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.altPhone
|
||||||
|
? `<tr><td><strong>대리인 전화번호</strong></td><td>${board.altPhone}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.companions
|
||||||
|
? `<tr><td><strong>동반 인원</strong></td><td>${board.companions}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.healthNotes
|
||||||
|
? `<tr><td><strong>건강/알레르기</strong></td><td>${board.healthNotes}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${
|
||||||
|
board.remarks
|
||||||
|
? `<tr><td><strong>기타 요청사항</strong></td><td>${board.remarks}</td></tr>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
<hr />
|
||||||
|
<p>이 메일은 예약 신청 확인용으로 자동 전송되었습니다.</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return { subject, html };
|
||||||
|
}
|
||||||
5501
bobu/package-lock.json
generated
5501
bobu/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@
|
|||||||
"@nuxtjs/sitemap": "^7.2.7",
|
"@nuxtjs/sitemap": "^7.2.7",
|
||||||
"@nuxtjs/tailwindcss": "^6.13.2",
|
"@nuxtjs/tailwindcss": "^6.13.2",
|
||||||
"@pinia/nuxt": "^0.10.1",
|
"@pinia/nuxt": "^0.10.1",
|
||||||
|
"@sendgrid/mail": "^8.1.5",
|
||||||
"@vee-validate/i18n": "^4.15.0",
|
"@vee-validate/i18n": "^4.15.0",
|
||||||
"@vee-validate/nuxt": "^4.15.0",
|
"@vee-validate/nuxt": "^4.15.0",
|
||||||
"@vee-validate/rules": "^4.15.0",
|
"@vee-validate/rules": "^4.15.0",
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
"firebase-functions": "^6.3.2",
|
"firebase-functions": "^6.3.2",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"npm": "^11.2.0",
|
"npm": "^11.2.0",
|
||||||
"nuxt": "^3.16.0",
|
"nuxt": "^3.17.0",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
|
|||||||
109
functions/package-lock.json
generated
109
functions/package-lock.json
generated
@@ -6,7 +6,8 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "functions",
|
"name": "functions",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"firebase-admin": "^13.3.0",
|
"@sendgrid/mail": "^8.1.5",
|
||||||
|
"firebase-admin": "^13.4.0",
|
||||||
"firebase-functions": "^6.3.2"
|
"firebase-functions": "^6.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1995,6 +1996,44 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@sendgrid/client": {
|
||||||
|
"version": "8.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.5.tgz",
|
||||||
|
"integrity": "sha512-Jqt8aAuGIpWGa15ZorTWI46q9gbaIdQFA21HIPQQl60rCjzAko75l3D1z7EyjFrNr4MfQ0StusivWh8Rjh10Cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sendgrid/helpers": "^8.0.0",
|
||||||
|
"axios": "^1.8.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sendgrid/helpers": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"deepmerge": "^4.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sendgrid/mail": {
|
||||||
|
"version": "8.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.5.tgz",
|
||||||
|
"integrity": "sha512-W+YuMnkVs4+HA/bgfto4VHKcPKLc7NiZ50/NH2pzO6UHCCFuq8/GNB98YJlLEr/ESDyzAaDr7lVE7hoBwFTT3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sendgrid/client": "^8.1.5",
|
||||||
|
"@sendgrid/helpers": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.27.8",
|
"version": "0.27.8",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||||
@@ -2869,8 +2908,7 @@
|
|||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
@@ -2888,6 +2926,33 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios/node_modules/form-data": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-jest": {
|
"node_modules/babel-jest": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||||
@@ -3394,7 +3459,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
},
|
},
|
||||||
@@ -3604,9 +3668,7 @@
|
|||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -3652,7 +3714,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
@@ -3912,7 +3973,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -4654,9 +4714,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/firebase-admin": {
|
"node_modules/firebase-admin": {
|
||||||
"version": "13.3.0",
|
"version": "13.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.4.0.tgz",
|
||||||
"integrity": "sha512-MFxv86Aw2rjM/TpKwU86jN7YUFfN1jy6mREYZTLL1aW1rCpZFi4c70b9U12J9Xa4RbJkiXpWBAwth9IVSqR91A==",
|
"integrity": "sha512-Y8DcyKK+4pl4B93ooiy1G8qvdyRMkcNFfBSh+8rbVcw4cW8dgG0VXCCTp5NUwub8sn9vSPsOwpb9tE2OuFmcfQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/busboy": "^3.0.0",
|
"@fastify/busboy": "^3.0.0",
|
||||||
@@ -4743,6 +4803,26 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
|
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/for-each": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
@@ -5249,7 +5329,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@@ -7819,6 +7898,12 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
},
|
},
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"firebase-admin": "^13.3.0",
|
"@sendgrid/mail": "^8.1.5",
|
||||||
|
"firebase-admin": "^13.4.0",
|
||||||
"firebase-functions": "^6.3.2"
|
"firebase-functions": "^6.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ import { initializeApp, getApps } from 'firebase-admin/app';
|
|||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
import { logger } from 'firebase-functions/v2';
|
import { logger } from 'firebase-functions/v2';
|
||||||
import { cookieExpiration, corsMiddlewareHandler } from '../config';
|
import { cookieExpiration, corsMiddlewareHandler } from '../config';
|
||||||
|
|
||||||
// ✅ Admin SDK init
|
// ✅ Admin SDK init
|
||||||
if (!getApps().length) {
|
if (!getApps().length) {
|
||||||
initializeApp(); // ✅ auto uses service account in Firebase Functions
|
initializeApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createSession = onRequest(
|
export const createSession = onRequest(
|
||||||
{ region: 'asia-northeast3' },
|
{ region: 'asia-northeast3' },
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
return corsMiddlewareHandler(req, res, async () => {
|
return corsMiddlewareHandler(req, res, async () => {
|
||||||
logger.info('countBoards: CORS‑enabled function called');
|
// Fix this logger message: it should be about createSession, not countBoards
|
||||||
|
logger.info('createSession: CORS-enabled function called');
|
||||||
|
|
||||||
if (req.method !== 'POST') {
|
if (req.method !== 'POST') {
|
||||||
res.status(405).send('Method not allowed');
|
res.status(405).send('Method not allowed');
|
||||||
return;
|
return;
|
||||||
@@ -38,7 +41,7 @@ export const createSession = onRequest(
|
|||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: true,
|
secure: true,
|
||||||
maxAge: expiresIn / 1000,
|
maxAge: expiresIn / 1000,
|
||||||
sameSite: 'lax' as const,
|
sameSite: 'none' as const,
|
||||||
path: '/',
|
path: '/',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,6 +51,10 @@ export const createSession = onRequest(
|
|||||||
);
|
);
|
||||||
res.status(200).json({ success: true });
|
res.status(200).json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
'createSession: Invalid token or session cookie creation failed',
|
||||||
|
error,
|
||||||
|
); // Added more specific logging
|
||||||
res.status(401).send('Invalid token');
|
res.status(401).send('Invalid token');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
// In your verifySession.ts (the API endpoint Cloud Function)
|
||||||
import { onRequest } from 'firebase-functions/v2/https';
|
import { onRequest } from 'firebase-functions/v2/https';
|
||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
import { getAuth } from 'firebase-admin/auth';
|
import { getAuth } from 'firebase-admin/auth';
|
||||||
import { initializeApp, getApps } from 'firebase-admin/app';
|
import { initializeApp, getApps } from 'firebase-admin/app';
|
||||||
import { corsMiddlewareHandler } from '../config';
|
import { corsMiddlewareHandler } from '../config';
|
||||||
import { logger } from 'firebase-functions/v2';
|
import { logger } from 'firebase-functions/v2'; // Ensure logger is imported
|
||||||
|
|
||||||
// ✅ Ensure Admin SDK is initialized
|
|
||||||
if (!getApps().length) {
|
if (!getApps().length) {
|
||||||
initializeApp();
|
initializeApp();
|
||||||
}
|
}
|
||||||
@@ -14,17 +14,31 @@ export const verifySession = onRequest(
|
|||||||
{ region: 'asia-northeast3' },
|
{ region: 'asia-northeast3' },
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
return corsMiddlewareHandler(req, res, async () => {
|
return corsMiddlewareHandler(req, res, async () => {
|
||||||
logger.info('verifySession: Function invoked');
|
logger.info('verifySession: Function invoked'); // This log is already there
|
||||||
|
|
||||||
|
// --- ADD THESE LOGS ---
|
||||||
|
logger.info(
|
||||||
|
'verifySession (endpoint): Raw headers.cookie:',
|
||||||
|
req.headers.cookie,
|
||||||
|
);
|
||||||
|
const cookies = cookie.parse(req.headers.cookie || '');
|
||||||
|
logger.info('verifySession (endpoint): Parsed cookies object:', cookies);
|
||||||
|
const session = cookies.__session;
|
||||||
|
logger.info(
|
||||||
|
'verifySession (endpoint): __session cookie value:',
|
||||||
|
session ? 'Found' : 'Not Found',
|
||||||
|
);
|
||||||
|
// --- END ADDED LOGS ---
|
||||||
|
|
||||||
if (req.method !== 'GET') {
|
if (req.method !== 'GET') {
|
||||||
res.status(405).send('Method Not Allowed');
|
res.status(405).send('Method Not Allowed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookies = cookie.parse(req.headers.cookie || '');
|
|
||||||
const session = cookies.__session;
|
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
logger.warn(
|
||||||
|
'verifySession (endpoint): No __session cookie found, returning 401.',
|
||||||
|
); // Specific warn
|
||||||
res.status(401).send({ message: 'No session cookie' });
|
res.status(401).send({ message: 'No session cookie' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -32,14 +46,22 @@ export const verifySession = onRequest(
|
|||||||
try {
|
try {
|
||||||
const decoded = await getAuth().verifySessionCookie(session, true);
|
const decoded = await getAuth().verifySessionCookie(session, true);
|
||||||
const role = decoded.role || 0;
|
const role = decoded.role || 0;
|
||||||
|
logger.info(
|
||||||
|
'verifySession (endpoint): Session verified successfully for UID:',
|
||||||
|
decoded.uid,
|
||||||
|
); // Log success
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
uid: decoded.uid,
|
uid: decoded.uid,
|
||||||
email: decoded.email,
|
email: decoded.email,
|
||||||
role,
|
role,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
logger.warn('verifySession: Session invalid or expired', err);
|
// Use any for err to access message
|
||||||
|
logger.warn(
|
||||||
|
'verifySession (endpoint): Session invalid or expired, returning 401:',
|
||||||
|
err instanceof Error ? err.message : String(err),
|
||||||
|
); // Specific warn
|
||||||
res.status(401).send({ message: 'Invalid session' });
|
res.status(401).send({ message: 'Invalid session' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
73
functions/src/board/boardMail.ts
Normal file
73
functions/src/board/boardMail.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { onRequest } from 'firebase-functions/v2/https';
|
||||||
|
import { logger } from 'firebase-functions/v2';
|
||||||
|
import { defineSecret } from 'firebase-functions/params';
|
||||||
|
import { initializeApp, getApps } from 'firebase-admin/app';
|
||||||
|
import sgMail from '@sendgrid/mail';
|
||||||
|
|
||||||
|
import type { BoardAccessMode } from '../types/boardItem';
|
||||||
|
|
||||||
|
import { corsMiddlewareHandler, SendEmail } from '../config';
|
||||||
|
|
||||||
|
/* ─── Boiler-plate init ───────────────────────────────────────── */
|
||||||
|
if (!getApps().length) initializeApp();
|
||||||
|
const SENDGRID_API_KEY = defineSecret('SENDGRID_API_KEY');
|
||||||
|
let isSendGridInitialized = false;
|
||||||
|
|
||||||
|
/* ─── Cloud Function entry ────────────────────────────────────── */
|
||||||
|
export const boardMail = onRequest(
|
||||||
|
{
|
||||||
|
region: 'asia-northeast3',
|
||||||
|
secrets: [SENDGRID_API_KEY], // ❷ declare dependency
|
||||||
|
},
|
||||||
|
async (req, res) => {
|
||||||
|
return corsMiddlewareHandler(req, res, async () => {
|
||||||
|
/* one-time SendGrid init per cold start */
|
||||||
|
if (!isSendGridInitialized) {
|
||||||
|
const apiKey = process.env.SENDGRID_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
logger.error('SendGrid API key is undefined. Check secret setup.');
|
||||||
|
throw new Error('Missing SendGrid API key');
|
||||||
|
}
|
||||||
|
sgMail.setApiKey(apiKey);
|
||||||
|
isSendGridInitialized = true;
|
||||||
|
}
|
||||||
|
/* 1. Method guard --------------------------------------------------- */
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
res.status(405).send({ message: 'Method Not Allowed' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* 2. Extract params ------------------------------------------------- */
|
||||||
|
const { access = 'public', subject, html, action = '' } = req.body ?? {};
|
||||||
|
|
||||||
|
if (!['public', 'private', 'admin'].includes(access)) {
|
||||||
|
logger.warn(`Invalid access mode: ${access}`);
|
||||||
|
res.status(400).send({ message: 'Bad access mode' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveAccess = access as BoardAccessMode; // ❹ always public/param
|
||||||
|
logger.info('boardMail: effectiveAccess', effectiveAccess);
|
||||||
|
if (!subject || !html) {
|
||||||
|
res.status(400).send({ message: 'Missing subject or html' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sgMail.send({
|
||||||
|
to: SendEmail.to,
|
||||||
|
from: SendEmail.from,
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`[boardMail] Email sent successfully via SendGrid [${action}]`,
|
||||||
|
);
|
||||||
|
res.status(200).send({ message: 'Email sent' });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[boardMail] Error sending email', err as Error);
|
||||||
|
res.status(500).send({ message: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -26,32 +26,22 @@ export const fetchBoards = onRequest(
|
|||||||
return corsMiddlewareHandler(request, response, async () => {
|
return corsMiddlewareHandler(request, response, async () => {
|
||||||
logger.info('fetchBoards: CORS‑enabled function called');
|
logger.info('fetchBoards: CORS‑enabled function called');
|
||||||
|
|
||||||
/* 1.Method guard ------------------------------------------------------------------ */
|
/* 1.Method guard */
|
||||||
if (request.method !== 'GET') {
|
if (request.method !== 'GET') {
|
||||||
response.status(405).send({ message: 'Method Not Allowed' });
|
response.status(405).send({ message: 'Method Not Allowed' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2.Auth (optional) ---------------------------------------------------------------- */
|
/* 2.Read & validate query params EARLY */
|
||||||
let userRole = 0;
|
|
||||||
let userId: string | null = null;
|
|
||||||
|
|
||||||
const authUser = await verifySessionFromRequest(request);
|
|
||||||
if (authUser) {
|
|
||||||
userId = authUser.uid;
|
|
||||||
userRole = authUser.role;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 3.Read & validate query params --------------------------------------------------- */
|
|
||||||
const {
|
const {
|
||||||
collection = '',
|
collection = '',
|
||||||
sortOrder = 'desc',
|
sortOrder = 'desc',
|
||||||
itemsPerPage = '10',
|
itemsPerPage = '10',
|
||||||
access = 'public',
|
access = 'public', // Default to 'public' if not provided
|
||||||
pageToken,
|
pageToken,
|
||||||
} = request.query as Record<string, string>;
|
} = request.query as Record<string, string>;
|
||||||
|
|
||||||
// ❶collection allow‑list
|
// ❶ collection allow-list
|
||||||
if (
|
if (
|
||||||
!ALLOWED_COLLECTIONS.has(
|
!ALLOWED_COLLECTIONS.has(
|
||||||
collection as unknown as typeof ALLOWED_COLLECTIONS extends Set<
|
collection as unknown as typeof ALLOWED_COLLECTIONS extends Set<
|
||||||
@@ -65,25 +55,45 @@ export const fetchBoards = onRequest(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ❷access must be one of three strings
|
// ❷ access must be one of three strings. Validate BEFORE auth for basic correctness.
|
||||||
if (!['public', 'private', 'admin'].includes(access)) {
|
if (!['public', 'private', 'admin'].includes(access)) {
|
||||||
response.status(400).send({ message: 'Bad access mode' });
|
response.status(400).send({ message: 'Bad access mode' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 3. Authentication and Authorization Check */
|
||||||
|
let userRole = 0;
|
||||||
|
let userId: string | null = null;
|
||||||
|
let authUser = null; // Initialize authUser to null
|
||||||
|
|
||||||
|
try {
|
||||||
|
authUser = await verifySessionFromRequest(request);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('fetchBoards: Session verification failed', err);
|
||||||
|
}
|
||||||
|
if (authUser) {
|
||||||
|
userId = authUser.uid;
|
||||||
|
userRole = authUser.role;
|
||||||
|
}
|
||||||
|
// If user is NOT logged in (userId is null) AND they are requesting 'private' or 'admin' access
|
||||||
|
if (!userId && (access === 'private' || access === 'admin')) {
|
||||||
|
response
|
||||||
|
.status(401)
|
||||||
|
.send({ message: 'Unauthorized: Login required for this resource.' });
|
||||||
|
return; // Function stops here.
|
||||||
|
}
|
||||||
|
|
||||||
// ❸numeric arguments
|
// ❸numeric arguments
|
||||||
const limit = Math.max(1, Math.min(Number(itemsPerPage) || 10, 100));
|
const limit = Math.max(1, Math.min(Number(itemsPerPage) || 10, 100));
|
||||||
const order = sortOrder === 'asc' ? 'asc' : 'desc';
|
const order = sortOrder === 'asc' ? 'asc' : 'desc';
|
||||||
|
|
||||||
/* 4.Build query with access control ----------------------------------------------- */
|
/* 4.Build query with access control ----------------------------------------------- */
|
||||||
const accessParam = (access ?? 'public') as BoardAccessMode;
|
|
||||||
const effectiveAccess = userId ? accessParam : 'public'; // force public if anonymous
|
|
||||||
|
|
||||||
let queryRef: Query<DocumentData>;
|
let queryRef: Query<DocumentData>;
|
||||||
try {
|
try {
|
||||||
queryRef = buildQueryWithAccessControl(
|
queryRef = buildQueryWithAccessControl(
|
||||||
db.collection(collection),
|
db.collection(collection),
|
||||||
effectiveAccess,
|
access as BoardAccessMode, // Use the original 'access'
|
||||||
userId,
|
userId,
|
||||||
userRole,
|
userRole,
|
||||||
);
|
);
|
||||||
@@ -141,7 +151,7 @@ export const fetchBoards = onRequest(
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (effectiveAccess === 'public')
|
if (access === 'public')
|
||||||
response.set('Cache-Control', 'public,max-age=60');
|
response.set('Cache-Control', 'public,max-age=60');
|
||||||
|
|
||||||
response.status(200).send({ items, nextPageToken });
|
response.status(200).send({ items, nextPageToken });
|
||||||
|
|||||||
112
functions/src/board/fetchSingleItem.ts
Normal file
112
functions/src/board/fetchSingleItem.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { onRequest } from 'firebase-functions/v2/https';
|
||||||
|
import { logger } from 'firebase-functions/v2';
|
||||||
|
import { getFirestore } from 'firebase-admin/firestore';
|
||||||
|
import { initializeApp, getApps } from 'firebase-admin/app';
|
||||||
|
import type { BoardItem, BoardAccessMode } from '../types/boardItem';
|
||||||
|
|
||||||
|
import { corsMiddlewareHandler, ALLOWED_COLLECTIONS } from '../config';
|
||||||
|
import {
|
||||||
|
isValidBoardItem,
|
||||||
|
verifySessionFromRequest,
|
||||||
|
buildQueryWithAccessControl,
|
||||||
|
} from '../utils';
|
||||||
|
|
||||||
|
if (!getApps().length) {
|
||||||
|
initializeApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = getFirestore();
|
||||||
|
|
||||||
|
export const fetchSingleItem = onRequest(
|
||||||
|
{ region: 'asia-northeast3' },
|
||||||
|
async (request, response) => {
|
||||||
|
return corsMiddlewareHandler(request, response, async () => {
|
||||||
|
logger.info('fetchSingleItem: CORS-enabled function called');
|
||||||
|
|
||||||
|
/* 1.Method guard ------------------------------------------------------------------ */
|
||||||
|
if (request.method !== 'GET') {
|
||||||
|
response.status(405).send({ message: 'Method Not Allowed' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Auth (optional)
|
||||||
|
let userRole = 0;
|
||||||
|
let userId: string | null = null;
|
||||||
|
|
||||||
|
const authUser = await verifySessionFromRequest(request);
|
||||||
|
if (authUser) {
|
||||||
|
userId = authUser.uid;
|
||||||
|
userRole = authUser.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Query params
|
||||||
|
const {
|
||||||
|
collection = '',
|
||||||
|
docId = '',
|
||||||
|
access = 'public',
|
||||||
|
} = request.query as Record<string, string>;
|
||||||
|
|
||||||
|
if (!collection || !docId) {
|
||||||
|
response.status(400).send({ message: 'Missing parameters' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❶collection allow‑list
|
||||||
|
if (
|
||||||
|
!ALLOWED_COLLECTIONS.has(
|
||||||
|
collection as unknown as typeof ALLOWED_COLLECTIONS extends Set<
|
||||||
|
infer U
|
||||||
|
>
|
||||||
|
? U
|
||||||
|
: never,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
response.status(400).send({ message: 'Invalid collection' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['public', 'private', 'admin'].includes(access)) {
|
||||||
|
response.status(400).send({ message: 'Bad access mode' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const accessParam = (access ?? 'public') as BoardAccessMode;
|
||||||
|
const effectiveAccess = userId ? accessParam : 'public'; // force public if anonymous
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build query with access control
|
||||||
|
const query = buildQueryWithAccessControl(
|
||||||
|
db.collection(collection),
|
||||||
|
effectiveAccess,
|
||||||
|
userId,
|
||||||
|
userRole,
|
||||||
|
).where('__name__', '==', docId);
|
||||||
|
|
||||||
|
const snap = await query.limit(1).get();
|
||||||
|
|
||||||
|
if (snap.empty) {
|
||||||
|
response
|
||||||
|
.status(404)
|
||||||
|
.send({ message: 'Item not found or access denied' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docSnap = snap.docs[0];
|
||||||
|
const data = docSnap.data() as BoardItem;
|
||||||
|
if (!isValidBoardItem(data)) {
|
||||||
|
response.status(500).send({ message: 'Invalid item data' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access === 'public') {
|
||||||
|
response.set('Cache-Control', 'public,max-age=60');
|
||||||
|
}
|
||||||
|
|
||||||
|
response.status(200).send({ ...data, docId: docSnap.id });
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('fetchSingleItem: Firestore error', err);
|
||||||
|
response.status(500).send({ message: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
export * from './countBoard';
|
export * from './countBoard';
|
||||||
export * from './fetchBoard';
|
export * from './fetchBoard';
|
||||||
|
export * from './fetchSingleItem';
|
||||||
|
export * from './boardMail';
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const MAX_FILE_SIZE_MB = 20;
|
|||||||
const MAX_TOTAL_FILES = 10;
|
const MAX_TOTAL_FILES = 10;
|
||||||
export const ALLOWED_COLLECTIONS = new Set([
|
export const ALLOWED_COLLECTIONS = new Set([
|
||||||
'notices',
|
'notices',
|
||||||
|
'wadizes' /* … */,
|
||||||
'projects' /* … */,
|
'projects' /* … */,
|
||||||
] as const);
|
] as const);
|
||||||
const ALLOWED_IMAGE_TYPES = [
|
const ALLOWED_IMAGE_TYPES = [
|
||||||
@@ -123,6 +124,12 @@ const ALLOWED_FILE_TYPES = [
|
|||||||
|
|
||||||
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
|
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
|
||||||
|
|
||||||
|
export const SendEmail = {
|
||||||
|
to: 'bobu1104@naver.com',
|
||||||
|
from: 'movemama@manos.kr',
|
||||||
|
subject: '[보부웹앱] - 새로운 신청',
|
||||||
|
};
|
||||||
|
|
||||||
const isImageFile = (file: File): boolean =>
|
const isImageFile = (file: File): boolean =>
|
||||||
ALLOWED_IMAGE_TYPES.includes(file.type);
|
ALLOWED_IMAGE_TYPES.includes(file.type);
|
||||||
const isVideoFile = (file: File): boolean =>
|
const isVideoFile = (file: File): boolean =>
|
||||||
|
|||||||
@@ -111,6 +111,23 @@ export type BoardItem = {
|
|||||||
thumbnail?: ImageItem; //Leave it for previous data structure
|
thumbnail?: ImageItem; //Leave it for previous data structure
|
||||||
};
|
};
|
||||||
// Board : Types
|
// Board : Types
|
||||||
|
export interface WadizBoard extends BoardItem {
|
||||||
|
name: string;
|
||||||
|
paymentId: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
address: string;
|
||||||
|
emergencyPhone: string;
|
||||||
|
scheduleStart?: string | Date;
|
||||||
|
scheduleEnd?: string | Date;
|
||||||
|
attending: 'yes' | 'no';
|
||||||
|
altName?: string;
|
||||||
|
altPhone?: string;
|
||||||
|
companions?: string;
|
||||||
|
healthNotes?: string;
|
||||||
|
remarks?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProjectBoard extends BoardItem {
|
export interface ProjectBoard extends BoardItem {
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
displayDate?: string;
|
displayDate?: string;
|
||||||
|
|||||||
@@ -1,33 +1,60 @@
|
|||||||
|
// Your verifyHelper.ts (or wherever this function is)
|
||||||
|
|
||||||
import { getAuth } from 'firebase-admin/auth';
|
import { getAuth } from 'firebase-admin/auth';
|
||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
|
import { logger } from 'firebase-functions/v2'; // <--- Make sure you import logger here
|
||||||
|
|
||||||
export type DecodedAuthInfo = {
|
export type DecodedAuthInfo = {
|
||||||
uid: string;
|
uid: string;
|
||||||
role: number;
|
role: number;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Verifies the session cookie from the request and returns the decoded auth info.
|
|
||||||
* Throws an error if invalid or missing.
|
|
||||||
* @param {Request} req - The Express request object containing the session cookie
|
|
||||||
* @param {Response} [res] - Optional Express response object for sending error responses
|
|
||||||
*/
|
|
||||||
export async function verifySessionFromRequest(
|
export async function verifySessionFromRequest(
|
||||||
req: Request,
|
req: Request,
|
||||||
): Promise<DecodedAuthInfo | null> {
|
): Promise<DecodedAuthInfo | null> {
|
||||||
|
// --- ADD THESE LOGS ---
|
||||||
|
logger.info('verifySessionFromRequest: STARTING verification');
|
||||||
|
logger.info(
|
||||||
|
'verifySessionFromRequest: Raw headers.cookie:',
|
||||||
|
req.headers.cookie,
|
||||||
|
); // Log the raw cookie header
|
||||||
|
// --- END ADDED LOGS ---
|
||||||
|
|
||||||
const cookies = cookie.parse(req.headers.cookie || '');
|
const cookies = cookie.parse(req.headers.cookie || '');
|
||||||
|
|
||||||
|
// --- ADD THESE LOGS ---
|
||||||
|
logger.info('verifySessionFromRequest: Parsed cookies object:', cookies); // Log the parsed cookies object
|
||||||
|
// --- END ADDED LOGS ---
|
||||||
|
|
||||||
const session = cookies.__session;
|
const session = cookies.__session;
|
||||||
|
|
||||||
|
// --- ADD THESE LOGS ---
|
||||||
|
logger.info(
|
||||||
|
'verifySessionFromRequest: __session cookie value:',
|
||||||
|
session ? 'Found (length: ' + session.length + ')' : 'Not Found',
|
||||||
|
);
|
||||||
|
// --- END ADDED LOGS ---
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
logger.warn('verifySessionFromRequest: No __session cookie found.'); // Specific log for missing cookie
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded = await getAuth().verifySessionCookie(session, true);
|
const decoded = await getAuth().verifySessionCookie(session, true);
|
||||||
const role = (decoded.role || 0) as number;
|
const role = (decoded.role || 0) as number;
|
||||||
|
logger.info(
|
||||||
|
'verifySessionFromRequest: Session cookie decoded successfully for UID:',
|
||||||
|
decoded.uid,
|
||||||
|
); // Log success
|
||||||
return { uid: decoded.uid, role };
|
return { uid: decoded.uid, role };
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
console.error('[Auth] Session cookie verification failed:', err);
|
// Use 'any' type for err to easily access its properties
|
||||||
|
logger.error(
|
||||||
|
'[Auth] Session cookie verification failed:',
|
||||||
|
err instanceof Error ? err.message : String(err),
|
||||||
|
); // Log the actual error message
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user