repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
certificates
stars
latest
clone command
git clone gitlawb://did:key:z6Mkqhmm...XL9c/certificatesgit clone gitlawb://did:key:z6Mkqhmm.../certificates019974a8sync from playground13h ago| #1 | rules_version = '2'; |
| #2 | service cloud.firestore { |
| #3 | match /databases/{database}/documents { |
| #4 | |
| #5 | // ─── Certificates collection ─────────────────────────────────── |
| #6 | match /certificates/{certId} { |
| #7 | |
| #8 | // Anyone can READ a certificate (public verification page). |
| #9 | // No auth required so the /#/verify/:id page works for anyone. |
| #10 | allow read: if true; |
| #11 | |
| #12 | // Only authenticated users may CREATE certificates. |
| #13 | // Data must match the expected schema and pass validation. |
| #14 | allow create: if request.auth != null |
| #15 | && isValidCertificate(request.resource.data); |
| #16 | |
| #17 | // Only the original issuer may UPDATE their own certificates, |
| #18 | // and only to change the status field (revoke). |
| #19 | allow update: if request.auth != null |
| #20 | && request.resource.data.issuedBy == request.auth.uid |
| #21 | && request.resource.data.status in ["active", "revoked"] |
| #22 | // Only the status field may change on update |
| #23 | && request.resource.data.diff(resource.data).affectedKeys() |
| #24 | .hasOnly(["status"]); |
| #25 | |
| #26 | // No one may delete certificates from the client SDK. |
| #27 | // Deletes should be done via Admin SDK / Firebase Console. |
| #28 | allow delete: if false; |
| #29 | } |
| #30 | |
| #31 | // ─── Deny everything else ────────────────────────────────────── |
| #32 | // No other collections are accessible from the client. |
| #33 | match /{document=**} { |
| #34 | allow read, write: if false; |
| #35 | } |
| #36 | |
| #37 | // ─── Validation helper ───────────────────────────────────────── |
| #38 | function isValidCertificate(data) { |
| #39 | return data.keys().hasAll([ |
| #40 | "id", "recipientName", "recipientEmail", "courseTitle", |
| #41 | "organization", "issueDate", "expiryDate", "templateId", |
| #42 | "customText", "status", "createdAt", "issuedBy" |
| #43 | ]) |
| #44 | // Type checks |
| #45 | && data.id is string |
| #46 | && data.recipientName is string |
| #47 | && data.recipientEmail is string |
| #48 | && data.courseTitle is string |
| #49 | && data.organization is string |
| #50 | && data.issueDate is string |
| #51 | && (data.expiryDate == null || data.expiryDate is string) |
| #52 | && data.templateId is string |
| #53 | && data.customText is string |
| #54 | && data.status is string |
| #55 | && data.createdAt is string |
| #56 | && data.issuedBy is string |
| #57 | // Optional uploaded cert fields |
| #58 | && (!(request.resource.data.keys().hasAll(["certType"])) || data.certType in ["generated", "uploaded"]) |
| #59 | && (!(request.resource.data.keys().hasAll(["fileData"])) || data.fileData is string) |
| #60 | && (!(request.resource.data.keys().hasAll(["fileType"])) || data.fileType is string) |
| #61 | && (!(request.resource.data.keys().hasAll(["fileName"])) || data.fileName is string) |
| #62 | // Length limits (prevent abuse) |
| #63 | && data.recipientName.size() <= 200 |
| #64 | && data.recipientEmail.size() <= 300 |
| #65 | && data.courseTitle.size() <= 500 |
| #66 | && data.organization.size() <= 300 |
| #67 | && data.customText.size() <= 2000 |
| #68 | && data.templateId in ["1","2","3","4","5","6","7","8","9","10","upload"] |
| #69 | && data.status in ["active", "revoked"] |
| #70 | // issuedBy must match the authenticated user |
| #71 | && data.issuedBy == request.auth.uid |
| #72 | } |
| #73 | } |
| #74 | } |
| #75 |