Vấn đề
Tôi muốn có cách đơn giản để bạn bè cùng làm album ảnh. Mỗi album gồm nhiều trang, mỗi trang chứa ảnh và chữ đặt tự do trên canvas. Một người sở hữu album, người khác tham gia với vai trò editor hoặc viewer, và album làm xong có thể xuất bản để ai cũng xem được.
Nghe như một app CRUD nhỏ, nhưng quy tắc phân quyền lan ra rất nhanh. Bản nháp chỉ chủ sở hữu và collaborator thấy. Editor được sửa album, viewer thì không. Chỉ chủ sở hữu được xóa hoặc xuất bản. Ảnh thuộc về người tải lên. Nếu mỗi endpoint tự kiểm tra, chỉ cần quên một chỗ là lộ album riêng tư. Dự án để private và đang được vài người bạn sử dụng.
Tôi đã làm gì
Chia mỗi domain theo cùng một kiểu lớp
Backend có một module cho mỗi domain: album, trang, media, collaborator và user. Module nào cũng cùng cấu trúc. Repository ghi dữ liệu, selector đọc dữ liệu, service giữ quy tắc nghiệp vụ, view thì mỏng. Một base service generic cho mọi module cùng các thao tác cơ bản. Đánh đổi là nhiều file và một ít boilerplate cho app cỡ này. Bù lại, quy tắc nào cũng có chỗ rõ ràng, và thêm module mới rất nhanh.
Đặt quy tắc phân quyền ở tầng service
Mọi lần đọc album đều đi qua một phương thức service trả lời câu hỏi “user này có được xem album này không?”. Thứ tự kiểm tra: album đã xuất bản thì ai cũng xem được, nếu không thì user phải đăng nhập, sau đó chủ sở hữu được qua, rồi đến collaborator. Sửa, xóa và xuất bản có kiểm tra owner hoặc editor riêng, và ném ra lỗi domain để API chuyển thành response lỗi. Đánh đổi: database không tự áp các quy tắc này, nên đường code nào bỏ qua service là bỏ qua luôn bước kiểm tra.
Giao việc đăng nhập cho identity provider
Tôi không muốn lưu mật khẩu. Frontend đăng nhập qua Auth0, còn API xác thực token đã ký ở mỗi request: chọn đúng public key, kiểm tra chữ ký, audience, issuer và thời hạn, rồi tạo user nội bộ ở lần đăng nhập đầu, gắn với id người dùng của provider. Đánh đổi là phụ thuộc cứng vào provider. Bản đầu của tôi còn tải signing key ở mọi request, phần này tôi nói ở dưới.
Soft delete và khóa UUID cho mọi model
Mọi model dùng chung một lớp cơ sở với khóa chính UUID, timestamp và soft delete theo cascade. Xóa album sẽ ẩn các trang và liên kết của nó chứ không xóa hẳn, nên lỡ tay vẫn khôi phục được. UUID giúp id khó đoán trong link chia sẻ. Cái giá là dòng đã xóa vẫn nằm trong database và mọi query phải tôn trọng cờ đã xóa, việc này thư viện lo.
Deploy từ repository
Mỗi lần push lên main sẽ chạy một workflow sao chép cấu hình của app từ environment được bảo vệ của repository sang dịch vụ hosting, rồi kích hoạt deploy. Ảnh nằm trên image CDN; database chỉ lưu link và metadata. Đánh đổi là cấu hình giờ nằm ở hai nơi, với repository là nguồn chuẩn.
Kết quả
FotoVerse chạy dưới dạng web app React và TypeScript trên nền Django REST API, có trình chỉnh trang kéo thả và đổi kích thước, chế độ xem lật trang, vai trò collaborator và xuất bản. Vài người bạn đang dùng. Cấu trúc module theo lớp hoạt động đủ tốt để tôi dùng lại trong một dự án cá nhân sau đó.
Nếu làm lại
Hiện chưa có test tự động, trong khi quy tắc phân quyền chính là thứ đáng test đầu tiên, mỗi trường hợp owner, editor, viewer và người ẩn danh một test. Bước kiểm tra token tải signing key của provider ở mọi request, không cache và không timeout, nên provider chậm thì mọi request đều chậm; tôi sẽ cache key và chỉ làm mới khi gặp key id lạ. Một README đầy đủ cũng sẽ giúp người khác chạy được dự án.