Lashtw commited on
Commit
55c2535
·
verified ·
1 Parent(s): 3addf9f

Upload 10 files

Browse files
src/services/auth.js ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth, googleProvider, db } from "./firebase.js";
2
+ import { signInWithPopup, signOut } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js";
3
+ import {
4
+ doc,
5
+ getDoc,
6
+ setDoc,
7
+ updateDoc,
8
+ collection,
9
+ getDocs,
10
+ deleteDoc,
11
+ serverTimestamp,
12
+ query
13
+ } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
14
+
15
+ const INSTRUCTORS_COLLECTION = "instructors";
16
+ const SUPER_ADMIN_EMAIL = "t92206@gmail.com";
17
+
18
+ /**
19
+ * Sign in with Google
20
+ */
21
+ export async function signInWithGoogle() {
22
+ try {
23
+ const result = await signInWithPopup(auth, googleProvider);
24
+ return result.user;
25
+ } catch (error) {
26
+ console.error("Google Sign-In Error:", error);
27
+ throw error;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Sign out
33
+ */
34
+ export async function signOutUser() {
35
+ try {
36
+ await signOut(auth);
37
+ } catch (error) {
38
+ console.error("Sign Out Error:", error);
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Check if user is an instructor and get permissions
45
+ * Bootstraps the Super Admin if not exists
46
+ * @param {object} user - Firebase User object
47
+ * @returns {Promise<object|null>} Instructor data or null if not authorized
48
+ */
49
+ export async function checkInstructorPermission(user) {
50
+ if (!user || !user.email) return null;
51
+
52
+ const email = user.email;
53
+ const instructorRef = doc(db, INSTRUCTORS_COLLECTION, email);
54
+ const snap = await getDoc(instructorRef);
55
+
56
+ // Bootstrap Super Admin
57
+ if (email === SUPER_ADMIN_EMAIL) {
58
+ const adminData = {
59
+ name: user.displayName || "Super Admin",
60
+ email: email,
61
+ role: 'admin',
62
+ permissions: ['create_room', 'add_question', 'manage_instructors'],
63
+ lastLogin: serverTimestamp()
64
+ };
65
+
66
+ if (!snap.exists()) {
67
+ await setDoc(instructorRef, {
68
+ ...adminData,
69
+ createdAt: serverTimestamp()
70
+ });
71
+ } else {
72
+ // Ensure admin always has full permissions
73
+ await updateDoc(instructorRef, {
74
+ role: 'admin',
75
+ permissions: ['create_room', 'add_question', 'manage_instructors'],
76
+ lastLogin: serverTimestamp()
77
+ });
78
+ }
79
+ return adminData;
80
+ }
81
+
82
+ if (snap.exists()) {
83
+ const data = snap.data();
84
+ await updateDoc(instructorRef, { lastLogin: serverTimestamp() });
85
+ return data;
86
+ }
87
+
88
+ return null; // Not an instructor
89
+ }
90
+
91
+ /**
92
+ * Get all instructors (Admin Only)
93
+ */
94
+ export async function getInstructors() {
95
+ const q = query(collection(db, INSTRUCTORS_COLLECTION));
96
+ const snapshot = await getDocs(q);
97
+ return snapshot.docs.map(doc => doc.data());
98
+ }
99
+
100
+ /**
101
+ * Add new instructor (Admin Only)
102
+ */
103
+ export async function addInstructor(email, name, permissions) {
104
+ const instructorRef = doc(db, INSTRUCTORS_COLLECTION, email);
105
+ await setDoc(instructorRef, {
106
+ email,
107
+ name,
108
+ role: 'instructor',
109
+ permissions,
110
+ createdAt: serverTimestamp()
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Update instructor (Admin Only)
116
+ */
117
+ export async function updateInstructor(email, data) {
118
+ const instructorRef = doc(db, INSTRUCTORS_COLLECTION, email);
119
+ await updateDoc(instructorRef, data);
120
+ }
121
+
122
+ /**
123
+ * Remove instructor (Admin Only)
124
+ */
125
+ export async function removeInstructor(email) {
126
+ if (email === SUPER_ADMIN_EMAIL) throw new Error("Cannot remove Super Admin");
127
+ await deleteDoc(doc(db, INSTRUCTORS_COLLECTION, email));
128
+ }
src/services/firebase.js CHANGED
@@ -1,5 +1,6 @@
1
  import { initializeApp } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js";
2
  import { getFirestore } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
 
3
 
4
  const firebaseConfig = {
5
  apiKey: "AIzaSyDq2a-yE6pbaeNf6KzkTcogh9oi-6nQbKk",
@@ -12,5 +13,7 @@ const firebaseConfig = {
12
 
13
  const app = initializeApp(firebaseConfig);
14
  const db = getFirestore(app);
 
 
15
 
16
- export { db };
 
1
  import { initializeApp } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js";
2
  import { getFirestore } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
3
+ import { getAuth, GoogleAuthProvider } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js";
4
 
5
  const firebaseConfig = {
6
  apiKey: "AIzaSyDq2a-yE6pbaeNf6KzkTcogh9oi-6nQbKk",
 
13
 
14
  const app = initializeApp(firebaseConfig);
15
  const db = getFirestore(app);
16
+ const auth = getAuth(app);
17
+ const googleProvider = new GoogleAuthProvider();
18
 
19
+ export { db, auth, googleProvider };
src/views/AdminView.js CHANGED
@@ -1,4 +1,6 @@
1
  import { getChallenges, createChallenge, updateChallenge, deleteChallenge } from "../services/classroom.js";
 
 
2
 
3
  export function renderAdminView() {
4
  return `
@@ -66,6 +68,22 @@ export function renderAdminView() {
66
  }
67
 
68
  export function setupAdminEvents() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  loadChallenges();
70
 
71
  document.getElementById('back-instructor-btn').addEventListener('click', () => {
 
1
  import { getChallenges, createChallenge, updateChallenge, deleteChallenge } from "../services/classroom.js";
2
+ import { checkInstructorPermission } from "../services/auth.js";
3
+ import { auth } from "../services/firebase.js";
4
 
5
  export function renderAdminView() {
6
  return `
 
68
  }
69
 
70
  export function setupAdminEvents() {
71
+ // Permission Check
72
+ const user = auth.currentUser;
73
+ if (!user) {
74
+ alert("請先登入");
75
+ window.location.hash = ''; // Back to Landing
76
+ return;
77
+ }
78
+
79
+ checkInstructorPermission(user).then(inst => {
80
+ if (!inst || !inst.permissions?.includes('add_question')) {
81
+ alert("您沒有權限管理題目");
82
+ window.location.hash = 'instructor';
83
+ return;
84
+ }
85
+ });
86
+
87
  loadChallenges();
88
 
89
  document.getElementById('back-instructor-btn').addEventListener('click', () => {
src/views/InstructorView.js CHANGED
@@ -1,4 +1,5 @@
1
  import { createRoom, subscribeToRoom, getChallenges, resetProgress, removeUser } from "../services/classroom.js";
 
2
  import { generateMonsterSVG, getNextMonster, MONSTER_DEFS } from "../utils/monsterUtils.js";
3
 
4
  // Load html-to-image dynamically (Better support than html2canvas)
@@ -19,10 +20,51 @@ export async function renderInstructorView() {
19
 
20
  return `
21
  <div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-90 backdrop-blur z-50 flex items-center justify-center">
22
- <div class="bg-gray-800 p-8 rounded-xl border border-gray-600 shadow-2xl max-w-sm w-full">
23
- <h2 class="text-xl font-bold text-center mb-6 text-white">🔒 講師身分驗證</h2>
24
- <input type="password" id="instructor-password" class="w-full bg-gray-900 border border-gray-700 rounded p-3 text-white text-center text-lg tracking-widest mb-4 focus:border-cyan-500 focus:outline-none" placeholder="輸密碼">
25
- <button id="auth-btn" class="w-full bg-purple-600 hover:bg-purple-500 text-white font-bold py-3 rounded-lg transition-colors">確認進入</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  </div>
27
  </div>
28
 
@@ -218,7 +260,10 @@ export async function renderInstructorView() {
218
  <button id="group-photo-btn" class="hidden bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-500 hover:to-purple-500 text-white font-bold py-2 px-4 rounded-lg transition-all shadow-lg border border-pink-400/30 flex items-center space-x-2">
219
  <span>📸 大合照</span>
220
  </button>
221
- <button id="nav-admin-btn" class="bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg transition-all border border-gray-600">
 
 
 
222
  管理題目
223
  </button>
224
  <div id="create-room-container" class="flex items-center space-x-2">
@@ -262,66 +307,183 @@ export async function renderInstructorView() {
262
 
263
  export function setupInstructorEvents() {
264
  let roomUnsubscribe = null;
 
265
 
266
- // Auth Logic
267
- const authBtn = document.getElementById('auth-btn');
268
- const pwdInput = document.getElementById('instructor-password');
269
  const authModal = document.getElementById('auth-modal');
 
270
 
271
- // Define Kick Function globally (robust against auth flow)
272
- window.confirmKick = async (userId, nickname) => {
273
- if (confirm(`確定要踢出 ${nickname} 嗎?此動作無法復原。`)) {
274
- try {
275
- const { removeUser } = await import("../services/classroom.js");
276
- await removeUser(userId);
277
- // UI will update automatically via subscribeToRoom
278
- } catch (e) {
279
- console.error("Kick failed:", e);
280
- alert("移除失敗");
281
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  }
283
- };
284
 
285
- // Default password check
286
- const checkPassword = async () => {
287
- const { verifyInstructorPassword } = await import("../services/classroom.js");
 
 
 
288
 
289
- authBtn.textContent = "驗證中...";
290
- authBtn.disabled = true;
 
 
 
 
 
291
 
 
 
292
  try {
293
- const isValid = await verifyInstructorPassword(pwdInput.value);
294
- if (isValid) {
 
 
 
 
 
295
  authModal.classList.add('hidden');
296
- // Store session to avoid re-login on reload
297
- sessionStorage.setItem('vibecoding_instructor_auth', 'true');
 
298
  } else {
299
- alert('密碼錯誤');
300
- pwdInput.value = '';
 
301
  }
302
- } catch (e) {
303
- console.error(e);
304
- alert("驗證出錯");
 
305
  } finally {
306
- authBtn.textContent = "確認進入";
307
  authBtn.disabled = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  }
309
  };
310
 
311
- authBtn.addEventListener('click', checkPassword);
312
- pwdInput.addEventListener('keypress', (e) => {
313
- if (e.key === 'Enter') checkPassword();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  });
315
 
316
- const createBtn = document.getElementById('create-room-btn');
317
- const roomInfo = document.getElementById('room-info');
318
- const createContainer = document.getElementById('create-room-container');
319
- const dashboardContent = document.getElementById('dashboard-content');
320
- const displayRoomCode = document.getElementById('display-room-code');
321
- const navAdminBtn = document.getElementById('nav-admin-btn');
322
- const groupPhotoBtn = document.getElementById('group-photo-btn');
323
- const snapshotBtn = document.getElementById('snapshot-btn');
324
- let isSnapshotting = false;
 
 
 
 
 
325
 
326
  // Snapshot Logic
327
  snapshotBtn.addEventListener('click', async () => {
@@ -705,15 +867,11 @@ export function setupInstructorEvents() {
705
  });
706
 
707
  // Logout Logic
708
- document.getElementById('logout-btn').addEventListener('click', () => {
709
  if (confirm('確定要登出講師模式嗎? (將會回到首頁)')) {
710
- sessionStorage.removeItem('vibecoding_instructor_auth');
711
  sessionStorage.removeItem('vibecoding_instructor_in_room');
712
  sessionStorage.removeItem('vibecoding_admin_referer');
713
- // We can optionally clear room history or keep it
714
- // localStorage.removeItem('vibecoding_instructor_room');
715
-
716
- // Clear hash to trigger main router back to Landing or default
717
  window.location.hash = '';
718
  window.location.reload();
719
  }
@@ -732,10 +890,10 @@ export function setupInstructorEvents() {
732
  }
733
  });
734
 
735
- // Check Previous Session
736
- if (sessionStorage.getItem('vibecoding_instructor_auth') === 'true') {
737
- authModal.classList.add('hidden');
738
- }
739
 
740
  // Check Active Room State
741
  const activeRoom = sessionStorage.getItem('vibecoding_instructor_in_room');
 
1
  import { createRoom, subscribeToRoom, getChallenges, resetProgress, removeUser } from "../services/classroom.js";
2
+ import { signInWithGoogle, signOutUser, checkInstructorPermission, getInstructors, addInstructor, updateInstructor, removeInstructor } from "../services/auth.js";
3
  import { generateMonsterSVG, getNextMonster, MONSTER_DEFS } from "../utils/monsterUtils.js";
4
 
5
  // Load html-to-image dynamically (Better support than html2canvas)
 
20
 
21
  return `
22
  <div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-90 backdrop-blur z-50 flex items-center justify-center">
23
+ <div class="bg-gray-800 p-8 rounded-xl border border-gray-600 shadow-2xl max-w-sm w-full text-center">
24
+ <h2 class="text-xl font-bold mb-6 text-white">🔒 講師登入</h2>
25
+ <p class="text-gray-400 mb-6 text-sm">請使用已授權的 Google 帳號登</p>
26
+ <button id="google-auth-btn" class="w-full bg-white text-gray-900 font-bold py-3 rounded-lg flex items-center justify-center space-x-2 hover:bg-gray-100 transition-colors">
27
+ <img src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg" class="w-5 h-5">
28
+ <span>Sign in with Google</span>
29
+ </button>
30
+ <p id="auth-error-msg" class="text-red-500 mt-4 hidden text-sm"></p>
31
+ </div>
32
+ </div>
33
+
34
+ <!-- Instructor Management Modal -->
35
+ <div id="instructor-modal" class="fixed inset-0 bg-gray-900/95 backdrop-blur z-[80] hidden flex flex-col items-center justify-center p-4">
36
+ <div class="bg-gray-800 rounded-xl border border-gray-700 shadow-2xl w-full max-w-4xl max-h-[90vh] flex flex-col">
37
+ <div class="p-6 border-b border-gray-700 flex justify-between items-center">
38
+ <h2 class="text-2xl font-bold text-white">👥 管理講師權限 (Manage Instructors)</h2>
39
+ <button onclick="document.getElementById('instructor-modal').classList.add('hidden')" class="text-gray-400 hover:text-white text-2xl">✕</button>
40
+ </div>
41
+
42
+ <div class="p-6 flex-1 overflow-y-auto">
43
+ <div class="mb-6 flex space-x-2">
44
+ <input type="email" id="new-inst-email" placeholder="Email (Gmail)" class="bg-gray-900 border border-gray-600 text-white px-4 py-2 rounded flex-1">
45
+ <input type="text" id="new-inst-name" placeholder="姓名" class="bg-gray-900 border border-gray-600 text-white px-4 py-2 rounded w-32">
46
+ <div class="flex items-center space-x-2 text-sm text-gray-300 bg-gray-900 px-3 rounded border border-gray-600">
47
+ <label><input type="checkbox" id="perm-room" checked> 開房</label>
48
+ <label><input type="checkbox" id="perm-q" checked> 題目</label>
49
+ <label><input type="checkbox" id="perm-inst"> 管理人</label>
50
+ </div>
51
+ <button id="btn-add-inst" class="bg-green-600 hover:bg-green-500 text-white px-4 py-2 rounded font-bold">新增</button>
52
+ </div>
53
+
54
+ <table class="w-full text-left border-collapse">
55
+ <thead>
56
+ <tr class="text-gray-400 border-b border-gray-700">
57
+ <th class="p-3">姓名</th>
58
+ <th class="p-3">Email</th>
59
+ <th class="p-3">權限</th>
60
+ <th class="p-3">動作</th>
61
+ </tr>
62
+ </thead>
63
+ <tbody id="instructor-list-body" class="text-gray-300">
64
+ <!-- Dynamic list -->
65
+ </tbody>
66
+ </table>
67
+ </div>
68
  </div>
69
  </div>
70
 
 
260
  <button id="group-photo-btn" class="hidden bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-500 hover:to-purple-500 text-white font-bold py-2 px-4 rounded-lg transition-all shadow-lg border border-pink-400/30 flex items-center space-x-2">
261
  <span>📸 大合照</span>
262
  </button>
263
+ <button id="nav-instructors-btn" class="hidden bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-2 px-4 rounded-lg transition-all border border-indigo-400/30 mr-2">
264
+ 👥 管理講師
265
+ </button>
266
+ <button id="nav-admin-btn" class="hidden bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg transition-all border border-gray-600">
267
  管理題目
268
  </button>
269
  <div id="create-room-container" class="flex items-center space-x-2">
 
307
 
308
  export function setupInstructorEvents() {
309
  let roomUnsubscribe = null;
310
+ let currentInstructor = null;
311
 
312
+ // UI References
313
+ const authBtn = document.getElementById('google-auth-btn');
 
314
  const authModal = document.getElementById('auth-modal');
315
+ const authErrorMsg = document.getElementById('auth-error-msg');
316
 
317
+ // Core Buttons
318
+ const createBtn = document.getElementById('create-room-btn');
319
+ const navAdminBtn = document.getElementById('nav-admin-btn');
320
+ const navInstBtn = document.getElementById('nav-instructors-btn');
321
+
322
+ // Other UI
323
+ const roomInfo = document.getElementById('room-info');
324
+ const createContainer = document.getElementById('create-room-container');
325
+ const dashboardContent = document.getElementById('dashboard-content');
326
+ const displayRoomCode = document.getElementById('display-room-code');
327
+ const groupPhotoBtn = document.getElementById('group-photo-btn');
328
+ const snapshotBtn = document.getElementById('snapshot-btn');
329
+ let isSnapshotting = false;
330
+
331
+ // Permission Check Helper
332
+ const checkPermissions = (instructor) => {
333
+ if (!instructor) return;
334
+
335
+ currentInstructor = instructor;
336
+
337
+ // 1. Create Room Permission
338
+ if (instructor.permissions?.includes('create_room')) {
339
+ createBtn.classList.remove('hidden', 'opacity-50', 'cursor-not-allowed');
340
+ createBtn.disabled = false;
341
+ } else {
342
+ createBtn.classList.add('opacity-50', 'cursor-not-allowed');
343
+ createBtn.disabled = true;
344
+ createBtn.title = "無此權限";
345
  }
 
346
 
347
+ // 2. Add Question Permission (Admin Button)
348
+ if (instructor.permissions?.includes('add_question')) {
349
+ navAdminBtn.classList.remove('hidden');
350
+ } else {
351
+ navAdminBtn.classList.add('hidden');
352
+ }
353
 
354
+ // 3. Manage Instructors Permission
355
+ if (instructor.permissions?.includes('manage_instructors')) {
356
+ navInstBtn.classList.remove('hidden');
357
+ } else {
358
+ navInstBtn.classList.add('hidden');
359
+ }
360
+ };
361
 
362
+ // Google Auth Logic
363
+ authBtn.addEventListener('click', async () => {
364
  try {
365
+ authBtn.disabled = true;
366
+ authBtn.classList.add('opacity-50');
367
+
368
+ const user = await signInWithGoogle();
369
+ const instructorData = await checkInstructorPermission(user);
370
+
371
+ if (instructorData) {
372
  authModal.classList.add('hidden');
373
+ checkPermissions(instructorData);
374
+ // Save name for avatar
375
+ localStorage.setItem('vibecoding_instructor_name', instructorData.name);
376
  } else {
377
+ authErrorMsg.textContent = "未授權的帳號 (Unauthorized Account)";
378
+ authErrorMsg.classList.remove('hidden');
379
+ await signOutUser();
380
  }
381
+ } catch (error) {
382
+ console.error(error);
383
+ authErrorMsg.textContent = "登入失敗: " + error.message;
384
+ authErrorMsg.classList.remove('hidden');
385
  } finally {
 
386
  authBtn.disabled = false;
387
+ authBtn.classList.remove('opacity-50');
388
+ }
389
+ });
390
+
391
+ // Handle Instructor Management
392
+ navInstBtn.addEventListener('click', async () => {
393
+ const modal = document.getElementById('instructor-modal');
394
+ const listBody = document.getElementById('instructor-list-body');
395
+
396
+ // Load list
397
+ const instructors = await getInstructors();
398
+ listBody.innerHTML = instructors.map(inst => `
399
+ <tr class="border-b border-gray-700 hover:bg-gray-800">
400
+ <td class="p-3">${inst.name}</td>
401
+ <td class="p-3 font-mono text-sm text-cyan-400">${inst.email}</td>
402
+ <td class="p-3 text-xs">
403
+ ${inst.permissions?.map(p => {
404
+ const map = { create_room: '開房', add_question: '題目', manage_instructors: '管理' };
405
+ return `<span class="bg-gray-700 px-2 py-1 rounded mr-1">${map[p] || p}</span>`;
406
+ }).join('')}
407
+ </td>
408
+ <td class="p-3">
409
+ ${inst.role === 'admin' ? '<span class="text-gray-500 italic">不可移除</span>' :
410
+ `<button class="text-red-400 hover:text-red-300" onclick="window.removeInst('${inst.email}')">移除</button>`}
411
+ </td>
412
+ </tr>
413
+ `).join('');
414
+
415
+ modal.classList.remove('hidden');
416
+ });
417
+
418
+ // Add New Instructor
419
+ document.getElementById('btn-add-inst').addEventListener('click', async () => {
420
+ const email = document.getElementById('new-inst-email').value.trim();
421
+ const name = document.getElementById('new-inst-name').value.trim();
422
+
423
+ if (!email || !name) return alert("請輸入完整資料");
424
+
425
+ const perms = [];
426
+ if (document.getElementById('perm-room').checked) perms.push('create_room');
427
+ if (document.getElementById('perm-q').checked) perms.push('add_question');
428
+ if (document.getElementById('perm-inst').checked) perms.push('manage_instructors');
429
+
430
+ try {
431
+ await addInstructor(email, name, perms);
432
+ alert("新增成功");
433
+ navInstBtn.click(); // Reload list
434
+ document.getElementById('new-inst-email').value = '';
435
+ document.getElementById('new-inst-name').value = '';
436
+ } catch (e) {
437
+ alert("新增失敗: " + e.message);
438
+ }
439
+ });
440
+
441
+ // Global helper for remove (hacky but works for simple onclick)
442
+ window.removeInst = async (email) => {
443
+ if (confirm(`確定移除 ${email}?`)) {
444
+ try {
445
+ await removeInstructor(email);
446
+ navInstBtn.click(); // Reload
447
+ } catch (e) {
448
+ alert(e.message);
449
+ }
450
  }
451
  };
452
 
453
+ // Auto Check Auth (Persistence)
454
+ // We rely on Firebase Auth state observer instead of session storage for security?
455
+ // Or we can just check if user is already signed in.
456
+ import("../services/firebase.js").then(({ auth }) => {
457
+ auth.onAuthStateChanged(async (user) => {
458
+ if (user) {
459
+ const instructorData = await checkInstructorPermission(user);
460
+ if (instructorData) {
461
+ authModal.classList.add('hidden');
462
+ checkPermissions(instructorData);
463
+ } else {
464
+ // Logged in google but not instructor
465
+ authModal.classList.remove('hidden');
466
+ }
467
+ } else {
468
+ authModal.classList.remove('hidden');
469
+ }
470
+ });
471
  });
472
 
473
+ // Define Kick Function globally (robust against auth flow)
474
+ window.confirmKick = async (userId, nickname) => {
475
+ if (confirm(`確定要踢出 ${nickname} 嗎?此動作無法復原。`)) {
476
+ try {
477
+ const { removeUser } = await import("../services/classroom.js");
478
+ await removeUser(userId);
479
+ // UI will update automatically via subscribeToRoom
480
+ } catch (e) {
481
+ console.error("Kick failed:", e);
482
+ alert("移除失敗");
483
+ }
484
+ }
485
+ };
486
+
487
 
488
  // Snapshot Logic
489
  snapshotBtn.addEventListener('click', async () => {
 
867
  });
868
 
869
  // Logout Logic
870
+ document.getElementById('logout-btn').addEventListener('click', async () => {
871
  if (confirm('確定要登出講師模式嗎? (將會回到首頁)')) {
872
+ await signOutUser();
873
  sessionStorage.removeItem('vibecoding_instructor_in_room');
874
  sessionStorage.removeItem('vibecoding_admin_referer');
 
 
 
 
875
  window.location.hash = '';
876
  window.location.reload();
877
  }
 
890
  }
891
  });
892
 
893
+ // Check Previous Session (Handled by onAuthStateChanged now)
894
+ // if (sessionStorage.getItem('vibecoding_instructor_auth') === 'true') {
895
+ // authModal.classList.add('hidden');
896
+ // }
897
 
898
  // Check Active Room State
899
  const activeRoom = sessionStorage.getItem('vibecoding_instructor_in_room');