diff --git a/Interaction.js.html b/Interaction.js.html index 70b0a1a..6d990cf 100644 --- a/Interaction.js.html +++ b/Interaction.js.html @@ -31,7 +31,7 @@ els.undoBtn.addEventListener('click', () => app.historyModule.undo()); els.redoBtn.addEventListener('click', () => app.historyModule.redo()); - const adminOnlyMessage = '此功能需要管理員或課表建立者權限。
若有需求,請聯繫系統管理員。'; + const adminOnlyMessage = '在「所有課表」檢視模式下無法使用此功能。
請先切換到特定課表再進行操作。'; const setupAdminModal = (triggerBtn, modal, closeBtn, onOpen) => { app.modals.setupModalListeners(triggerBtn, modal, closeBtn, () => { diff --git a/ScheduleManager.js.html b/ScheduleManager.js.html index 01d4afd..97d20cd 100644 --- a/ScheduleManager.js.html +++ b/ScheduleManager.js.html @@ -345,17 +345,11 @@ }; App.canManageCurrentScheduleSettings = function() { - if (App.isCurrentUserAdmin()) { - return true; - } + // Ref: #152 — All logged-in users can manage schedules if (App.activeScheduleId === AppConfig.ALL_SCHEDULES_ID) { - return false; // Cannot manage settings in "All Schedules" view - } - const schedule = App.schedules[App.activeScheduleId]; - if (!schedule || !schedule.createdBy) { - return false; // No schedule or creator info + return false; // Cannot manage in "All Schedules" view } - return App.currentUserEmail === App.getShortUserName(schedule.createdBy); + return true; }; // --- Drag and drop --- diff --git a/tests/unit/backend.test.js b/tests/unit/backend.test.js index a745a74..5a7306b 100644 --- a/tests/unit/backend.test.js +++ b/tests/unit/backend.test.js @@ -179,12 +179,13 @@ describe('_checkPermission', () => { expect(() => gas._checkPermission('creator@school.com')).not.toThrow(); }); - it('rejects unauthorized user', () => { + // Ref: #152 — All logged-in users can now edit/delete/copy/rename + it('allows any logged-in user (not just admin/creator)', () => { const gas = createGasEnv({ userEmail: 'hacker@school.com', scriptProps: { ADMIN_EMAIL: 'admin@school.com' }, }); - expect(() => gas._checkPermission('creator@school.com')).toThrow('權限不足'); + expect(() => gas._checkPermission('creator@school.com')).not.toThrow(); }); it('case-insensitive email comparison', () => { @@ -208,7 +209,8 @@ describe('_checkPermission', () => { // ─── copySchedule (#41 permission check) ───────────────────────────────── describe('copySchedule', () => { - it('rejects copy by unauthorized user (Ref: #41)', () => { + // Ref: #152 — All logged-in users can copy + it('allows copy by any logged-in user (Ref: #152)', () => { const dataSheet = createMockSheet('Data', { A1: 'ID', B1: 'Name', C1: 'Modified', D1: 'CreatedBy', F1: '2024-01-01T00:00:00.000Z', @@ -229,8 +231,7 @@ describe('copySchedule', () => { metadataTimestamp: '2024-01-01T00:00:00.000Z', }); - expect(result.success).toBe(false); - expect(result.error).toContain('權限不足'); + expect(result.success).toBe(true); }); it('allows copy by owner', () => { @@ -465,15 +466,15 @@ describe('saveData', () => { expect(result.error).toContain('找不到'); }); - it('rejects unauthorized user', () => { + // Ref: #152 — All logged-in users can save + it('allows save by any logged-in user (Ref: #152)', () => { const gas = createSaveEnv({ userEmail: 'hacker@test.com' }); const result = gas.saveData({ scheduleId: 'schedule_1', scheduleData: { scheduleData: {}, classrooms: [], tags: [] }, lastModified: '2024-06-15T10:30:00.000Z', }); - expect(result.success).toBe(false); - expect(result.error).toContain('權限不足'); + expect(result.success).toBe(true); }); it('returns error on invalid payload (missing fields)', () => { @@ -521,14 +522,14 @@ describe('deleteSchedule', () => { expect(result.success).toBe(true); }); - it('rejects unauthorized user', () => { + // Ref: #152 — All logged-in users can delete + it('allows delete by any logged-in user (Ref: #152)', () => { const gas = createDeleteEnv({ userEmail: 'hacker@test.com' }); const result = gas.deleteSchedule({ id: 'schedule_1', metadataTimestamp: '2024-06-15T10:30:00.000Z', }); - expect(result.success).toBe(false); - expect(result.error).toContain('權限不足'); + expect(result.success).toBe(true); }); }); @@ -609,15 +610,15 @@ describe('updateScheduleMetadata', () => { expect(result.success).toBe(true); }); - it('rejects unauthorized user', () => { + // Ref: #152 — All logged-in users can rename + it('allows rename by any logged-in user (Ref: #152)', () => { const gas = createRenameEnv({ userEmail: 'hacker@test.com' }); const result = gas.updateScheduleMetadata({ id: 'schedule_1', - newName: 'Hacked', + newName: 'Renamed', metadataTimestamp: '2024-06-15T10:30:00.000Z', }); - expect(result.success).toBe(false); - expect(result.error).toContain('權限不足'); + expect(result.success).toBe(true); }); }); diff --git a/tests/unit/businessLogicEdgeCases.test.js b/tests/unit/businessLogicEdgeCases.test.js index 1a490d4..e743dfd 100644 --- a/tests/unit/businessLogicEdgeCases.test.js +++ b/tests/unit/businessLogicEdgeCases.test.js @@ -309,24 +309,26 @@ describe('_checkPermission — edge cases (#138)', () => { }); // --- Creator access --- - it('allows creator to manage their own schedule', () => { + // Ref: #152 — All logged-in users can manage, not just creator + it('allows any logged-in user, not just creator', () => { const gas = createGasEnv({ - userEmail: 'teacher@school.com', + userEmail: 'anyone@school.com', scriptProps: { ADMIN_EMAIL: 'admin@school.com' }, }); - expect(() => gas._checkPermission('teacher@school.com')).not.toThrow(); + expect(() => gas._checkPermission('creator@school.com')).not.toThrow(); }); - // --- Unauthorized access --- - it('rejects user who is neither admin nor creator', () => { + // --- Previously unauthorized access now allowed --- + // Ref: #152 — Permission model simplified + it('allows user who is neither admin nor creator (#152)', () => { const gas = createGasEnv({ userEmail: 'other@school.com', scriptProps: { ADMIN_EMAIL: 'admin@school.com' }, }); - expect(() => gas._checkPermission('creator@school.com')).toThrow('權限不足'); + expect(() => gas._checkPermission('creator@school.com')).not.toThrow(); }); - // --- Case sensitivity: admin email --- + // --- Case sensitivity: admin email (still valid — admin passes) --- it('admin check is case-insensitive (upper vs lower)', () => { const gas = createGasEnv({ userEmail: 'ADMIN@SCHOOL.COM', @@ -343,8 +345,8 @@ describe('_checkPermission — edge cases (#138)', () => { expect(() => gas._checkPermission('creator@school.com')).not.toThrow(); }); - // --- Case sensitivity: creator email --- - it('creator check is case-insensitive', () => { + // --- Case sensitivity: creator email (still valid — any logged-in user passes) --- + it('any logged-in user passes regardless of email case', () => { const gas = createGasEnv({ userEmail: 'Teacher@School.COM', scriptProps: { ADMIN_EMAIL: 'admin@school.com' }, @@ -352,15 +354,7 @@ describe('_checkPermission — edge cases (#138)', () => { expect(() => gas._checkPermission('teacher@school.com')).not.toThrow(); }); - it('creator check is case-insensitive (reversed case)', () => { - const gas = createGasEnv({ - userEmail: 'teacher@school.com', - scriptProps: { ADMIN_EMAIL: 'admin@school.com' }, - }); - expect(() => gas._checkPermission('TEACHER@SCHOOL.COM')).not.toThrow(); - }); - - // --- Empty email guard (Ref: #62) --- + // --- Empty email guard (Ref: #62) — still enforced --- it('throws "未登入" when current user email is empty', () => { const gas = createGasEnv({ userEmail: '', @@ -369,86 +363,38 @@ describe('_checkPermission — edge cases (#138)', () => { expect(() => gas._checkPermission('creator@school.com')).toThrow('未登入'); }); - // --- Missing ADMIN_EMAIL config --- - it('non-admin user passes when ADMIN_EMAIL config is missing', () => { - // getConfig('ADMIN_EMAIL') returns null/undefined - // (null || '').toLowerCase() = '' which won't match anyone - // So only creator match matters + // --- Missing ADMIN_EMAIL config — all logged-in users still pass (#152) --- + it('allows any logged-in user when ADMIN_EMAIL config is missing', () => { const gas = createGasEnv({ - userEmail: 'creator@school.com', + userEmail: 'anyone@school.com', scriptProps: {}, // no ADMIN_EMAIL }); expect(() => gas._checkPermission('creator@school.com')).not.toThrow(); }); - it('rejects non-creator when ADMIN_EMAIL config is missing', () => { - const gas = createGasEnv({ - userEmail: 'other@school.com', - scriptProps: {}, // no ADMIN_EMAIL - }); - expect(() => gas._checkPermission('creator@school.com')).toThrow('權限不足'); - }); - - it('no one is admin when ADMIN_EMAIL config is missing', () => { - // Even if user's email is something, without config it can't match - const gas = createGasEnv({ - userEmail: 'admin@school.com', - scriptProps: {}, // no ADMIN_EMAIL set - }); - // admin@school.com is NOT admin because config is empty - // But admin@school.com !== creator@school.com → reject - expect(() => gas._checkPermission('creator@school.com')).toThrow('權限不足'); - }); - - // --- ADMIN_EMAIL config is empty string --- - it('empty ADMIN_EMAIL config means no one is admin', () => { - const gas = createGasEnv({ - userEmail: 'user@school.com', - scriptProps: { ADMIN_EMAIL: '' }, - }); - // ''.toLowerCase() = '' — user@school.com !== '' so not admin - // user@school.com !== creator@school.com → reject - expect(() => gas._checkPermission('creator@school.com')).toThrow('權限不足'); - }); - - // --- createdBy is null/undefined --- - it('throws when createdBy is null (null.toLowerCase throws)', () => { + // --- createdBy is null/undefined — no longer throws (#152, createdBy not accessed) --- + it('does not throw when createdBy is null (#152 — createdBy unused)', () => { const gas = createGasEnv({ userEmail: 'user@school.com', scriptProps: { ADMIN_EMAIL: 'admin@school.com' }, }); - // createdBy.toLowerCase() will throw TypeError on null - expect(() => gas._checkPermission(null)).toThrow(); + expect(() => gas._checkPermission(null)).not.toThrow(); }); - it('throws when createdBy is undefined', () => { + it('does not throw when createdBy is undefined (#152 — createdBy unused)', () => { const gas = createGasEnv({ userEmail: 'user@school.com', scriptProps: { ADMIN_EMAIL: 'admin@school.com' }, }); - expect(() => gas._checkPermission(undefined)).toThrow(); + expect(() => gas._checkPermission(undefined)).not.toThrow(); }); - // --- Admin with null createdBy should still pass (admin bypass) --- - it('admin can pass even with null createdBy (admin check comes first)', () => { - const gas = createGasEnv({ - userEmail: 'admin@school.com', - scriptProps: { ADMIN_EMAIL: 'admin@school.com' }, - }); - // isAdmin=true → short-circuits before createdBy.toLowerCase() - // Wait: the code does `!isAdmin && currentUser.toLowerCase() !== createdBy.toLowerCase()` - // isAdmin=true → !isAdmin=false → AND short-circuits → no throw - expect(() => gas._checkPermission(null)).not.toThrow(); - }); - - // --- Whitespace in emails --- - it('does not trim whitespace in emails (documents behavior)', () => { + // --- Whitespace in emails — still passes (#152, only login check matters) --- + it('allows user with whitespace in email (#152 — only login check)', () => { const gas = createGasEnv({ userEmail: ' admin@school.com ', scriptProps: { ADMIN_EMAIL: 'admin@school.com' }, }); - // ' admin@school.com '.toLowerCase() !== 'admin@school.com' (whitespace matters) - // Not admin, not creator → reject - expect(() => gas._checkPermission('creator@school.com')).toThrow('權限不足'); + expect(() => gas._checkPermission('creator@school.com')).not.toThrow(); }); }); diff --git "a/\347\250\213\345\274\217\347\242\274.js" "b/\347\250\213\345\274\217\347\242\274.js" index 134c99d..04d24e2 100644 --- "a/\347\250\213\345\274\217\347\242\274.js" +++ "b/\347\250\213\345\274\217\347\242\274.js" @@ -44,10 +44,7 @@ function _checkPermission(createdBy) { const currentUser = Session.getActiveUser().getEmail(); // Ref: #62 — Guard against empty email (e.g. time-driven triggers return '') if (!currentUser) throw new Error('未登入,無法執行此操作'); - const isAdmin = currentUser.toLowerCase() === (getConfig('ADMIN_EMAIL') || '').toLowerCase(); - if (!isAdmin && currentUser.toLowerCase() !== createdBy.toLowerCase()) { - throw new Error("權限不足。只有管理員或建立者才能執行此操作。"); - } + // Ref: #152 — All logged-in users can edit/delete/copy/rename } /**