| rules_version = '2'; |
| service cloud.firestore { |
| match /databases/{database}/documents { |
| |
| |
| match /{document=**} { |
| allow read, write: if false; |
| } |
|
|
| |
| |
| 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; |
| } |
|
|
| |
| 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(); |
| 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']); |
| } |
|
|
| |
| 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(); |
| } |
|
|
| |
| 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(); |
| } |
|
|
| |
| 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()) && |
| ( |
| |
| (existing().status == 'draft' && !incoming().diff(existing()).affectedKeys().hasAll(['status'])) || |
| |
| incoming().diff(existing()).affectedKeys().hasOnly(['status']) |
| ); |
| allow delete: if isTeacher() && existing().teacherId == getUserId() && existing().status == 'draft'; |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| match /counters/{counterId} { |
| allow read, write: if true; |
| } |
| } |
| } |
|
|