2025-08-06 22:31:43 +05:00
|
|
|
package postgresql
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2026-03-11 16:00:34 +05:00
|
|
|
"hkp-clavis/internal/model"
|
|
|
|
|
repo "hkp-clavis/internal/repo/storage"
|
2025-08-06 22:31:43 +05:00
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
_ "embed"
|
|
|
|
|
|
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//go:embed structure.sql
|
|
|
|
|
var db_struct string
|
|
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
var (
|
|
|
|
|
insertUid = `
|
|
|
|
|
insert into pgp_uids(fingerprint, uid, email, verified, verification_token, token_expires_at)
|
|
|
|
|
values($1, $2, $3, $4, $5, $6)
|
|
|
|
|
on conflict (fingerprint, uid) do nothing;
|
|
|
|
|
`
|
|
|
|
|
selectUid = `
|
|
|
|
|
select uid, verification_token, token_expires_at, verified
|
|
|
|
|
from pgp_uids
|
|
|
|
|
where fingerprint = $1 and email = $2;
|
|
|
|
|
`
|
|
|
|
|
updateUidVerify = `
|
|
|
|
|
update pgp_uids
|
|
|
|
|
set verified = true,
|
|
|
|
|
verification_token = null,
|
|
|
|
|
token_expires_at = null
|
|
|
|
|
where fingerprint = $1 and email = $2 and verification_token = $3;
|
|
|
|
|
`
|
|
|
|
|
fetchUidVerifed = `
|
|
|
|
|
select uid, email, verification_token, token_expires_at, verified
|
|
|
|
|
from pgp_uids
|
|
|
|
|
where fingerprint = $1 and verified = true;
|
|
|
|
|
`
|
|
|
|
|
insertKey = `
|
|
|
|
|
insert into pgp_keys(fingerprint, packet, revoked, update_time)
|
|
|
|
|
values($1, $2, $3, now())
|
|
|
|
|
on conflict (fingerprint) do update set
|
|
|
|
|
packet = excluded.packet,
|
|
|
|
|
revoked = excluded.revoked,
|
|
|
|
|
update_time = now();
|
|
|
|
|
`
|
|
|
|
|
selectKeyByFingerprint = `
|
|
|
|
|
select fingerprint, packet, revoked, update_time
|
|
|
|
|
from pgp_keys
|
|
|
|
|
where fingerprint ~* $1;
|
|
|
|
|
`
|
|
|
|
|
indexKeyByString = `
|
|
|
|
|
SELECT
|
|
|
|
|
k.fingerprint, k.packet, k.revoked, k.update_time,
|
|
|
|
|
u.uid, u.email, u.verified, u.verification_token, u.token_expires_at
|
|
|
|
|
FROM pgp_keys k
|
|
|
|
|
JOIN pgp_uids u ON k.fingerprint = u.fingerprint
|
|
|
|
|
WHERE k.fingerprint IN (
|
|
|
|
|
SELECT DISTINCT fingerprint
|
|
|
|
|
FROM pgp_uids
|
|
|
|
|
WHERE (uid ~* $1 OR email ~* $1 OR fingerprint ~* $1) AND verified = true
|
|
|
|
|
)
|
|
|
|
|
ORDER BY k.fingerprint;
|
|
|
|
|
`
|
|
|
|
|
cleanupStaleKeys = `
|
|
|
|
|
delete from pgp_keys pk
|
|
|
|
|
where not exists (
|
|
|
|
|
select 1 from pgp_uids pu where pu.fingerprint = pk.fingerprint
|
|
|
|
|
);
|
|
|
|
|
`
|
|
|
|
|
)
|
|
|
|
|
|
2025-08-06 22:31:43 +05:00
|
|
|
type PostgresqlStorageRepo struct {
|
2026-03-11 16:00:34 +05:00
|
|
|
poll *pgxpool.Pool
|
|
|
|
|
keyMngr *model.KeyManager
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get context, url
|
|
|
|
|
// Return postgresql repository
|
2026-03-11 16:00:34 +05:00
|
|
|
func New(ctx context.Context, pool *pgxpool.Pool, keyManager *model.KeyManager) repo.StorageRepositotyInterface {
|
2025-08-06 22:31:43 +05:00
|
|
|
_, err := pool.Exec(ctx, db_struct)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return PostgresqlStorageRepo{
|
|
|
|
|
poll: pool,
|
2026-03-11 16:00:34 +05:00
|
|
|
// Allocation manager
|
|
|
|
|
keyMngr: keyManager,
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
func (s PostgresqlStorageRepo) AddKey(ctx context.Context, keys []*model.PGPKey) error {
|
2025-08-06 22:31:43 +05:00
|
|
|
tx, err := s.poll.Begin(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error postgresql: AddKey: begin transaction: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer tx.Rollback(ctx)
|
|
|
|
|
|
|
|
|
|
for _, k := range keys {
|
|
|
|
|
fingerprint := k.Fingerprint
|
|
|
|
|
// Upsert pgp_keys
|
2026-03-11 16:00:34 +05:00
|
|
|
_, err = tx.Exec(ctx, insertKey, fingerprint, k.Packet, k.Revoked)
|
2025-08-06 22:31:43 +05:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error postgresql: AddKey: upsert key %s: %w", fingerprint, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upsert pgp_uids
|
|
|
|
|
batch := &pgx.Batch{}
|
|
|
|
|
for _, u := range k.Uids {
|
2026-03-11 16:00:34 +05:00
|
|
|
batch.Queue(insertUid, fingerprint, u.UIDString, u.Email, u.Verify, u.Token, u.TokenExpires)
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
br := tx.SendBatch(ctx, batch)
|
|
|
|
|
// Consume all results from the batch to ensure completion
|
|
|
|
|
for i := 0; i < len(k.Uids); i++ {
|
|
|
|
|
_, err = br.Exec()
|
|
|
|
|
if err != nil {
|
|
|
|
|
br.Close()
|
|
|
|
|
return fmt.Errorf("error postgresql: AddKey: batch insert uid %d for key %s: %w", i, fingerprint, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
br.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tx.Commit(ctx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s PostgresqlStorageRepo) VerifyUID(ctx context.Context, fingerprint, email, token string) error {
|
|
|
|
|
tx, err := s.poll.Begin(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error postgresql: VerifyUID: begin transaction: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer tx.Rollback(ctx)
|
|
|
|
|
|
|
|
|
|
// SELECT the UID to verify.
|
|
|
|
|
var storedUID string // The full UID string (e.g., "Name <email>")
|
|
|
|
|
var storedToken sql.NullString
|
|
|
|
|
var tokenExpiresAt sql.NullTime
|
|
|
|
|
var isVerified bool
|
|
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
row := tx.QueryRow(ctx, selectUid, fingerprint, email)
|
2025-08-06 22:31:43 +05:00
|
|
|
|
|
|
|
|
if err = row.Scan(&storedUID, &storedToken, &tokenExpiresAt, &isVerified); err != nil {
|
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
// Not found
|
|
|
|
|
return fmt.Errorf("error postgresql: VerifyUID: invalid verification token or link")
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("error postgresql: VerifyUID: failed to scan UID data: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isVerified {
|
|
|
|
|
return fmt.Errorf("error postgresql: VerifyUID: UID for %s is already verified", email)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if storedToken.String == "" || storedToken.String != token {
|
|
|
|
|
return fmt.Errorf("error postgresql: VerifyUID: invalid verification token or link")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tokenExpiresAt.Time.Before(time.Now()) {
|
|
|
|
|
return fmt.Errorf("error postgresql: VerifyUID: verification token has expired")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UPDATE the UID status.
|
2026-03-11 16:00:34 +05:00
|
|
|
result, err := tx.Exec(ctx, updateUidVerify, fingerprint, email, token)
|
2025-08-06 22:31:43 +05:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error postgresql: VerifyUID: failed to update UID verification status: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rowsAffected := result.RowsAffected()
|
|
|
|
|
if rowsAffected == 0 {
|
|
|
|
|
return fmt.Errorf("error postgresql: VerifyUID: verification failed, token might be invalid or already used")
|
|
|
|
|
}
|
|
|
|
|
if rowsAffected > 1 {
|
|
|
|
|
return fmt.Errorf("error postgresql: VerifyUID: multiple UIDs affected by verification (data integrity error!)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tx.Commit(ctx)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
func (s PostgresqlStorageRepo) GetKey(ctx context.Context, fngprt string) (res []*model.PGPKey, err error) {
|
|
|
|
|
pgpKey := s.keyMngr.Get()
|
2025-08-06 22:31:43 +05:00
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
defer func() {
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.keyMngr.Release(pgpKey)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
row := s.poll.QueryRow(ctx, selectKeyByFingerprint, fngprt)
|
2025-08-06 22:31:43 +05:00
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
if err = row.Scan(&pgpKey.Fingerprint, &pgpKey.Packet, &pgpKey.Revoked, &pgpKey.UpdateTime); err != nil {
|
2025-08-06 22:31:43 +05:00
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
return nil, repo.ErrKeyNotFound
|
|
|
|
|
}
|
2026-03-11 16:00:34 +05:00
|
|
|
return nil, fmt.Errorf("error postgresql: GetKey: scan error: %w", err)
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
|
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
if !strings.Contains(pgpKey.Fingerprint, fngprt) {
|
2025-08-06 22:31:43 +05:00
|
|
|
return nil, repo.ErrKeyNotFound
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
rows, err := s.poll.Query(ctx, fetchUidVerifed, pgpKey.Fingerprint)
|
2025-08-06 22:31:43 +05:00
|
|
|
if err != nil {
|
2026-03-11 16:00:34 +05:00
|
|
|
return nil, fmt.Errorf("error postgresql: GetKey: query uids error: %w", err)
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
|
for rows.Next() {
|
2026-03-11 16:00:34 +05:00
|
|
|
uid := s.keyMngr.GetUid()
|
2025-08-06 22:31:43 +05:00
|
|
|
var token sql.NullString
|
|
|
|
|
var tokenExpiresAt sql.NullTime
|
2026-03-11 16:00:34 +05:00
|
|
|
|
|
|
|
|
if err = rows.Scan(&uid.UIDString, &uid.Email, &token, &tokenExpiresAt, &uid.Verify); err != nil {
|
|
|
|
|
s.keyMngr.ReleaseUid(uid)
|
|
|
|
|
return nil, err
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uid.Token = token.String
|
|
|
|
|
uid.TokenExpires = tokenExpiresAt.Time
|
2026-03-11 16:00:34 +05:00
|
|
|
pgpKey.Uids = append(pgpKey.Uids, uid)
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err = rows.Err(); err != nil {
|
2026-03-11 16:00:34 +05:00
|
|
|
return nil, err
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
|
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
return []*model.PGPKey{pgpKey}, nil
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
|
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
func (s PostgresqlStorageRepo) Index(ctx context.Context, q string) (resultKeys []*model.PGPKey, err error) {
|
|
|
|
|
rows, err := s.poll.Query(ctx, indexKeyByString, q)
|
2025-08-06 22:31:43 +05:00
|
|
|
if err != nil {
|
2026-03-11 16:00:34 +05:00
|
|
|
return nil, fmt.Errorf("postgresql: Index query failed: %w", err)
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
2026-03-11 16:00:34 +05:00
|
|
|
defer rows.Close()
|
2025-08-06 22:31:43 +05:00
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
defer func() {
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.keyMngr.Release(resultKeys...)
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
2026-03-11 16:00:34 +05:00
|
|
|
}()
|
2025-08-06 22:31:43 +05:00
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
var currentKey *model.PGPKey
|
|
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var fng, packet string
|
|
|
|
|
var revoked bool
|
|
|
|
|
var updateTime time.Time
|
|
|
|
|
|
|
|
|
|
uidObj := s.keyMngr.GetUid()
|
|
|
|
|
var token sql.NullString
|
|
|
|
|
var tokenExpiresAt sql.NullTime
|
2025-08-06 22:31:43 +05:00
|
|
|
|
2026-03-11 16:00:34 +05:00
|
|
|
err = rows.Scan(
|
|
|
|
|
&fng, &packet, &revoked, &updateTime,
|
|
|
|
|
&uidObj.UIDString, &uidObj.Email, &uidObj.Verify, &token, &tokenExpiresAt,
|
|
|
|
|
)
|
2025-08-06 22:31:43 +05:00
|
|
|
if err != nil {
|
2026-03-11 16:00:34 +05:00
|
|
|
s.keyMngr.ReleaseUid(uidObj)
|
|
|
|
|
return nil, err
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
2026-03-11 16:00:34 +05:00
|
|
|
|
|
|
|
|
uidObj.Token = token.String
|
|
|
|
|
uidObj.TokenExpires = tokenExpiresAt.Time
|
|
|
|
|
uidObj.Fingerprint = fng
|
|
|
|
|
|
|
|
|
|
if currentKey == nil || currentKey.Fingerprint != fng {
|
|
|
|
|
currentKey = s.keyMngr.Get()
|
|
|
|
|
currentKey.Fingerprint = fng
|
|
|
|
|
currentKey.Packet = packet
|
|
|
|
|
currentKey.Revoked = revoked
|
|
|
|
|
currentKey.UpdateTime = updateTime
|
|
|
|
|
|
|
|
|
|
resultKeys = append(resultKeys, currentKey)
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
2026-03-11 16:00:34 +05:00
|
|
|
|
|
|
|
|
currentKey.Uids = append(currentKey.Uids, uidObj)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err = rows.Err(); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("postgresql: Index rows iteration error: %w", err)
|
2025-08-06 22:31:43 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return resultKeys, nil
|
|
|
|
|
}
|
2026-03-11 16:00:34 +05:00
|
|
|
|
2025-08-06 22:31:43 +05:00
|
|
|
func (s PostgresqlStorageRepo) CleanupStaleKeys(ctx context.Context) (int, error) {
|
2026-03-11 16:00:34 +05:00
|
|
|
result, err := s.poll.Exec(ctx, cleanupStaleKeys)
|
2025-08-06 22:31:43 +05:00
|
|
|
if err != nil {
|
|
|
|
|
return 0, fmt.Errorf("error postgresql: CleanupStaleKeys: failed to delete stale keys: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rowsAffected := int(result.RowsAffected())
|
|
|
|
|
return rowsAffected, nil
|
|
|
|
|
}
|