Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

feat: add invite generation and validation

+73
+54
internal/collaboration/invite.go
··· 1 + package collaboration 2 + 3 + import ( 4 + "crypto/rand" 5 + "crypto/sha256" 6 + "encoding/hex" 7 + "fmt" 8 + "time" 9 + 10 + "github.com/limeleaf/diffdown/internal/db" 11 + "github.com/limeleaf/diffdown/internal/model" 12 + ) 13 + 14 + func GenerateInviteToken() (string, error) { 15 + bytes := make([]byte, 32) 16 + if _, err := rand.Read(bytes); err != nil { 17 + return "", err 18 + } 19 + hash := sha256.Sum256(bytes) 20 + return hex.EncodeToString(hash[:]), nil 21 + } 22 + 23 + func CreateInvite(database *db.DB, documentRKey, createdByDID string) (*model.Invite, error) { 24 + token, err := GenerateInviteToken() 25 + if err != nil { 26 + return nil, err 27 + } 28 + 29 + invite := &model.Invite{ 30 + ID: db.NewID(), 31 + DocumentRKey: documentRKey, 32 + Token: token, 33 + CreatedBy: createdByDID, 34 + CreatedAt: time.Now(), 35 + ExpiresAt: time.Now().Add(7 * 24 * time.Hour), 36 + } 37 + 38 + err = database.CreateInvite(invite) 39 + return invite, err 40 + } 41 + 42 + func ValidateInvite(database *db.DB, token, documentRKey string) (*model.Invite, error) { 43 + invite, err := database.GetInviteByToken(token) 44 + if err != nil { 45 + return nil, err 46 + } 47 + if invite.DocumentRKey != documentRKey { 48 + return nil, fmt.Errorf("invite does not match document") 49 + } 50 + if time.Now().After(invite.ExpiresAt) { 51 + return nil, fmt.Errorf("invite expired") 52 + } 53 + return invite, nil 54 + }
+19
internal/db/db.go
··· 156 156 return err 157 157 } 158 158 159 + // --- Invites --- 160 + 161 + func (db *DB) CreateInvite(invite *model.Invite) error { 162 + _, err := db.Exec(` 163 + INSERT INTO invites (id, document_rkey, token, created_by_did, created_at, expires_at) 164 + VALUES (?, ?, ?, ?, ?, ?)`, 165 + invite.ID, invite.DocumentRKey, invite.Token, invite.CreatedBy, invite.CreatedAt, invite.ExpiresAt) 166 + return err 167 + } 168 + 169 + func (db *DB) GetInviteByToken(token string) (*model.Invite, error) { 170 + row := db.QueryRow(`SELECT id, document_rkey, token, created_by_did, created_at, expires_at FROM invites WHERE token = ?`, token) 171 + var invite model.Invite 172 + err := row.Scan(&invite.ID, &invite.DocumentRKey, &invite.Token, &invite.CreatedBy, &invite.CreatedAt, &invite.ExpiresAt) 173 + if err != nil { 174 + return nil, err 175 + } 176 + return &invite, nil 177 + }