feat: Secure document sharing via Magic Link + OTP verification#22
feat: Secure document sharing via Magic Link + OTP verification#22Laefuu wants to merge 1 commit intokOlapsis:mainfrom
Conversation
…ngside new localization files and authentication middleware.
|
Hello Very interesting PR, thank you! In any case, thank you for your contribution! |
|
Hello, You’re welcome! The tool is pretty neat. It’s just a feature we needed, so we tried to build it for our specific use case (which is why it mainly works through the API). It kind of turned into a challenge for me to try and create it in an decent way. Feel free to take anything you find useful from it, I thought it might be helpful. I think OTP would be a great addition. It could be a good way to secure a "guest link" for sensitive content or allow users to log in safely. As for my profile, I’ve removed a lot of my previous projects. I mainly used Git for university work, and I rarely push to my personal GitHub since I don’t often get the chance to work on side projects and dev the days haha. But I’m considering doing it more, and your code was a great opportunity to start. Cheers, and thanks again for the tool :) |
Why
Ackify produces reports for clients who need to read and acknowledge them. Currently there's no way to share a private document with a client who doesn't have an account, without exposing them through a public link or onboarding them through full SSO.
We need a lightweight, secure sharing mechanism that:
What
A new API-driven document sharing feature that combines magic links with one-time password (OTP) verification. Backend only — no UI changes.
User flow
Security model
How
Architecture
Built on top of the existing
magic_link_tokenstable and MagicLinkService, extending them with OTP and revocation capabilities:graph TD A[Admin Handler] -->|CreateDocumentShareLink| B[AuthProvider Interface] B --> C[Dynamic Provider] C --> D[MagicLinkService] D --> E[MagicLinkRepository] E --> F[(magic_link_tokens table)] D --> G[Email Sender] H[Public Auth Handler] -->|ValidateDocumentShareToken| B H -->|VerifyDocumentShareOTP| B style A fill:#f9a825 style H fill:#66bb6aNew API endpoints
POST/api/v1/admin/documents/{docId}/share{otp, link}GET/api/v1/admin/documents/{docId}/sharesDELETE/api/v1/admin/documents/{docId}/share/{tokenId}GET/api/v1/auth/document-share/verify?token=...{status: "otp_required"}POST/api/v1/auth/document-share/verify-otp{redirect_to}Database changes
Migration
0020_add_document_share_otp:All columns are nullable/have defaults → zero impact on existing rows.
Files changed
New files
migrations/0020_add_document_share_otp.up.sqlmigrations/0020_add_document_share_otp.down.sqlModified files
/document-share/verifyfrom strict API CSPUI & Styling
connect-src 'self'for the frontend API fetch.Dependencies
golang.org/x/cryptobumped (already indirect dependency, now used directly forbcrypt)Side effects on existing code
Important
Carefully analyzed — no existing behavior is affected.
RevokedAt(NULL → passes), IsOTPLocked() (OTPMaxAttempts=5 default, OTPAttempts=0 → false),UsedAtguard hasPurpose != "document_share"→ login/reminder still single-useAND purpose != 'document_share'to unused-token cleanup. Without this, multi-use document shares (whereused_atstays NULL) would be deleted after 7 days even if valid for 90 days. Login/reminder tokens unaffected.Testing
Unit tests
Live end-to-end test (against real PostgreSQL)
GET /verify?token=<real>{"status":"otp_required"}POST /verify-otp {otp:"000000"}{"error":"Invalid or expired link or access code"}POST /verify-otp {otp:"546947"}{"redirect_to":"/?doc=test-document-001"}GET /verify?token=<same>{"status":"otp_required"}(multi-use works)GET /verify?token=fake{"error":"Invalid or expired link"}Build verification
go build ./backend/...— clean compilation, zero warningsWhat's next (not in this PR)