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
}
/**