examforge / firestore.rules
Benjahmin's picture
feat(auth): implement email-password authentication
20ba598
Raw
History Blame Contribute Delete
5.68 kB
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 0. Global Safety Net
match /{document=**} {
allow read, write: if false;
}
// --- Helpers ---
function isSignedIn() {
return request.auth != null;
}
function getUserId() {
return request.auth.token.email.endsWith('@examforge.com')
? request.auth.token.email.split('@')[0]
: request.auth.uid;
}
function getUserData(userId) {
return get(/databases/$(database)/documents/users/$(userId)).data;
}
function isTeacher() {
return isSignedIn() &&
request.auth.token.email != null &&
getUserData(getUserId()).role == 'teacher';
}
function isValidId(id) {
return id is string && id.size() <= 128 && id.matches('^[a-zA-Z0-9_\\-]+$');
}
function incoming() {
return request.resource.data;
}
function existing() {
return resource.data;
}
// --- User Validation ---
function isValidUser(data) {
return data.keys().hasAll(['id', 'fullName', 'role', 'createdAt']) &&
data.id is string && data.id.size() <= 64 &&
data.fullName is string && data.fullName.size() <= 100 &&
data.role in ['student', 'teacher'];
}
match /users/{userId} {
allow get: if isSignedIn() && (getUserId() == userId || isTeacher());
allow list: if isTeacher(); // Teachers can browse students for groups
allow create: if isSignedIn() && getUserId() == userId && isValidUser(incoming());
allow update: if isSignedIn() && getUserId() == userId &&
incoming().id == existing().id &&
incoming().role == existing().role &&
incoming().diff(existing()).affectedKeys().hasOnly(['fullName', 'class', 'department', 'position', 'subject', 'examHistory', 'groupIds']);
}
// --- Question Validation ---
function isValidQuestion(data) {
return data.keys().hasAll(['id', 'text', 'options', 'correctOptionId', 'teacherId', 'createdAt']) &&
data.teacherId == getUserId() &&
data.options.size() >= 2 && data.options.size() <= 8;
}
match /questions/{questionId} {
allow read: if isSignedIn();
allow create: if isTeacher() && isValidId(questionId) && isValidQuestion(incoming()) && incoming().createdAt == request.time;
allow update: if isTeacher() && existing().teacherId == getUserId() &&
isValidQuestion(incoming()) &&
incoming().teacherId == existing().teacherId;
allow delete: if isTeacher() && existing().teacherId == getUserId();
}
// --- Group Validation ---
function isValidGroup(data) {
return data.keys().hasAll(['id', 'name', 'teacherId', 'studentIds', 'createdAt']) &&
data.teacherId == getUserId() &&
data.name is string && data.name.size() > 0;
}
match /groups/{groupId} {
allow read: if isSignedIn() && (resource.data.teacherId == getUserId() || resource.data.studentIds.hasAny([getUserId()]));
allow create: if isTeacher() && isValidId(groupId) && isValidGroup(incoming()) && incoming().createdAt == request.time;
allow update: if isTeacher() && existing().teacherId == getUserId() &&
isValidGroup(incoming()) &&
incoming().teacherId == existing().teacherId;
allow delete: if isTeacher() && existing().teacherId == getUserId();
}
// --- Exam Validation ---
function isValidExam(data) {
return data.keys().hasAll(['id', 'title', 'teacherId', 'duration', 'status', 'createdAt', 'settings', 'questionIds']) &&
data.teacherId == getUserId() &&
data.duration is number && data.duration > 0;
}
match /exams/{examId} {
allow read: if isSignedIn();
allow create: if isTeacher() && isValidId(examId) && isValidExam(incoming()) && incoming().createdAt == request.time;
allow update: if isTeacher() && existing().teacherId == getUserId() &&
isValidExam(incoming()) &&
(
// Draft editing
(existing().status == 'draft' && !incoming().diff(existing()).affectedKeys().hasAll(['status'])) ||
// Quick status switch
incoming().diff(existing()).affectedKeys().hasOnly(['status'])
);
allow delete: if isTeacher() && existing().teacherId == getUserId() && existing().status == 'draft';
}
// --- Attempt Validation ---
match /attempts/{attemptId} {
allow list: if isSignedIn() && (resource.data.studentId == getUserId() || isTeacher());
allow get: if isSignedIn() && (resource.data.studentId == getUserId() || isTeacher());
allow create: if isSignedIn() && isValidId(attemptId) &&
incoming().studentId == getUserId() &&
incoming().startedAt == request.time &&
exists(/databases/$(database)/documents/exams/$(incoming().examId));
allow update: if isSignedIn() && existing().studentId == getUserId() &&
existing().status == 'started' &&
incoming().diff(existing()).affectedKeys().hasOnly(['answers', 'score', 'percentage', 'timeSpent', 'status', 'completedAt']) &&
(incoming().status != 'completed' || incoming().completedAt == request.time);
}
// --- Counter Validation ---
match /counters/{counterId} {
allow read, write: if true;
}
}
}