Nghiên cứu tình huống 2: Một công cụ SaaS nhỏ có xác thực (auth) và cơ sở dữ liệu
Ý tưởng
Một nhà thiết kế tự do (freelance) muốn có một bảng điều khiển (dashboard) riêng tư để ghi lại số giờ tính phí theo từng khách hàng và xuất ra một tệp CSV (Comma-Separated Values — một tệp bảng tính dạng văn bản thuần mà ứng dụng bảng tính nào cũng mở được) hàng tháng. Không có gì cầu kỳ, nhưng nó cần tài khoản (để dữ liệu của cô ấy là của riêng cô ấy) và khả năng lưu trữ lâu dài (persistence).
Bản đặc tả
Xác thực và cơ sở dữ liệu làm tăng mức độ rủi ro, nên bản đặc tả cụ thể hơn về ranh giới:
A logged-in web app where a user can: sign up / log in with email,
create clients, log time entries (date, client, hours, note),
see a table of entries filtered by month, and download that month
as CSV. Each user only ever sees their own data. Mobile-friendly.
"Each user only ever sees their own data" (Mỗi người dùng chỉ bao giờ được thấy dữ liệu của chính họ) trông giống một dòng về trải nghiệm người dùng (UX). Nhưng thực ra đó là mô hình bảo mật của toàn bộ ứng dụng, được nén lại trong chín từ. Việc ghi rõ điều đó trong bản đặc tả có nghĩa là chúng tôi có thể chỉ ngược lại vào đó mỗi khi AI đi lệch hướng.
Stack công nghệ
Với một người xây dựng đơn độc, nước đi thắng lợi là chọn một stack mà xác thực và cơ sở dữ liệu là các dịch vụ được quản lý sẵn (managed services), không phải là code do bạn tự viết. Chúng tôi chọn một ứng dụng Next.js triển khai trên một máy chủ , cùng với một cơ sở dữ liệu Postgres được lưu trữ sẵn (hosted) và một nhà cung cấp xác thực có thể cắm vào dùng ngay (drop-in), lo việc đăng nhập bằng email, phiên đăng nhập (), và đặt lại mật khẩu giúp chúng tôi. Càng ít code phải tự viết thì càng ít code mà một AI có thể viết sai thay chúng tôi. Xác thực đặc biệt là một hạng mục mà bạn hầu như không bao giờ muốn tự viết tay cùng một AI: các kiểu lỗi của nó âm thầm không ai hay, phạm vi ảnh hưởng là tài khoản của tất cả mọi người, và một nhà cung cấp được quản lý sẵn thì các trường hợp biên ( case) của họ đã được hàng triệu lượt đăng nhập thử thách rồi.
Các phần được quản lý sẵn nằm ở bên ngoài; quy tắc duy nhất mà bạn tự chịu trách nhiệm nằm ở giữa — mọi truy vấn (query) đều lọc theo user_id, để một người dùng không bao giờ có thể đọc được hàng dữ liệu của người khác:
┌──────┐ ┌─────────────┐ ┌──────────────────┐
│ USER │─ login ─▶│ AUTH │─ user ─▶│ NEXT.JS APP │
└──────┘ │ (managed) │ id │ │
└─────────────┘ │ every query: │
│ WHERE user_id=? ─┼──┐
└──────────────────┘ │
▼
┌──────────────────┐
│ POSTGRES (managed)│
│ clients · entries │
└──────────────────┘
↑ managed = less to get wrong ↑ the one boundary YOU enforce
Những câu lệnh then chốt
Chúng tôi để mẫu (template) có sẵn của nhà cung cấp xác thực lo phần việc nặng nhọc, rồi chỉ đạo AI xây thêm logic nghiệp vụ ( logic) lên trên:
We're using [auth provider]'s Next.js starter. Add a Postgres
schema with two tables: clients (id, user_id, name) and
time_entries (id, user_id, client_id, date, hours, note). Every
query MUST filter by the logged-in user's id from the session.
Generate the migration and the typed data-access functions.
Câu "MUST filter by user_id" (PHẢI lọc theo user_id) là câu quan trọng nhất trong toàn bộ dự án. Chúng tôi lặp lại ràng buộc đó trong gần như mọi liên quan đến dữ liệu, bởi vì lỗi đáng sợ nhất trong một ứng dụng nhiều người dùng là chuyện một người thấy được hàng dữ liệu của người khác. Sự lặp lại nghe có vẻ dư thừa khi bạn đang gõ nó ra; nhưng chính sự dư thừa đó lại là thứ cứu bạn, bởi mô hình không hề nhớ được ràng buộc đó quan trọng đến mức nào giữa các prompt riêng lẻ.
Cho phần xuất dữ liệu:
Add a /api/export route that takes a month (YYYY-MM), pulls the
logged-in user's time_entries for that month joined to client
names, and streams a CSV download. Reject the request if there's
no valid session.
Trở ngại
Khi kiểm thử, chúng tôi tạo hai tài khoản và phát hiện tài khoản B có thể thấy được các khách hàng của tài khoản A trong một danh sách xổ xuống (dropdown). Đây chính xác là lỗi mà chúng tôi lo sợ. Thay vì bảo AI "sửa nó đi", chúng tôi bắt nó chứng minh vấn đề trước:
Account B is seeing Account A's clients. Show me every database
query in the codebase that reads the clients table, and for each
one tell me whether it filters by the session user_id. Don't fix
anything yet — just audit.
Cuộc kiểm tra (audit) phát hiện ra một truy vấn — bộ nạp dữ liệu cho dropdown — đã được viết trước khi chúng tôi thêm ràng buộc đó và bị lọt lưới. Chúng tôi yêu cầu nó thêm bộ lọc còn thiếu, rồi yêu cầu thêm một lớp bảo vệ:
Add a single helper that every read goes through, which takes the
session and injects the user_id filter, so no future query can
forget it. Refactor the existing queries to use it.
Điều đó biến một bản sửa lỗi đơn lẻ thành một sự đảm bảo mang tính cấu trúc. Sau đó chúng tôi làm cho sự đảm bảo đó có thể kiểm chứng được, vì một lớp bảo vệ mà bạn không thể xác minh chỉ là một niềm hy vọng suông:
Write a test that creates two users, has each create a client,
then asserts that user A's session can never read user B's client
through any of the data-access functions.
Bài học: khi một AI gây ra một lỗi bảo mật, đừng chỉ vá riêng trường hợp đó — hãy chỉ đạo nó loại bỏ toàn bộ hạng mục lỗi đó, rồi khóa chặt hạng mục đó lại bằng một bài kiểm thử (test) sẽ báo lỗi ầm ĩ nếu có ai mở lại nó.
Ra mắt
Chúng tôi gieo (seed) dữ liệu thử cho một tháng, xuất ra tệp CSV, mở nó trong một ứng dụng bảng tính để xác nhận các con số và mã hóa ký tự (encoding) đều đúng, rồi đặt một mật khẩu cơ sở dữ liệu mạnh và xoay vòng (rotate) các thông tin đăng nhập ra khỏi mọi tệp cục bộ. Chúng tôi triển khai lên máy chủ serverless, thêm các biến môi trường sản phẩm trong bảng điều khiển của nó, và đưa cho cô ấy đường liên kết (). Cô ấy tự đăng ký với một lần đăng ký thật. Toàn bộ quá trình xây dựng gói gọn trong một cuối tuần.