package store import ( "context" "database/sql" "encoding/json" "errors" "time" "jarl/internal/model" ) const certCols = `id, name, source, domains, cert_file, key_file, uses, challenge, key_type, comment, cert_pem, key_pem, names, not_before, not_after, issuer, serial, renew_at, ari_checked_at, last_attempt, last_success, last_error, failures, last_routes, created_at, updated_at, created_by, updated_by, owner_id` func scanCert(sc interface{ Scan(...any) error }) (*model.Certificate, error) { c := &model.Certificate{} var domains, uses, names, routes, ca, ua string var nb, na, ra, ac, la, ls sql.NullString if err := sc.Scan(&c.ID, &c.Name, &c.Source, &domains, &c.CertFile, &c.KeyFile, &uses, &c.Challenge, &c.KeyType, &c.Comment, &c.CertPEM, &c.KeyPEM, &names, &nb, &na, &c.Issuer, &c.Serial, &ra, &ac, &la, &ls, &c.LastError, &c.Failures, &routes, &ca, &ua, &c.CreatedBy, &c.UpdatedBy, &c.OwnerID); err != nil { return nil, err } for _, f := range []struct { raw string dst *[]string }{{domains, &c.Domains}, {uses, &c.Uses}, {names, &c.Names}, {routes, &c.LastRoutes}} { if err := json.Unmarshal([]byte(f.raw), f.dst); err != nil { return nil, err } if *f.dst == nil { *f.dst = []string{} } } c.NotBefore, c.NotAfter, c.RenewAt, c.ARICheckedAt = parseTSPtr(nb), parseTSPtr(na), parseTSPtr(ra), parseTSPtr(ac) c.LastAttempt, c.LastSuccess = parseTSPtr(la), parseTSPtr(ls) c.CreatedAt, c.UpdatedAt = parseTS(ca), parseTS(ua) return c, nil } // Certificates returns every certificate, by name. func (s *Store) Certificates(ctx context.Context) ([]*model.Certificate, error) { rows, err := s.db.QueryContext(ctx, `SELECT `+certCols+` FROM certificates ORDER BY name`) if err != nil { return nil, err } out := []*model.Certificate{} for rows.Next() { c, err := scanCert(rows) if err != nil { rows.Close() return nil, err } out = append(out, c) } return out, closeRows(rows) } // CertificateByName returns the certificate called name. func (s *Store) CertificateByName(ctx context.Context, name string) (*model.Certificate, error) { c, err := scanCert(s.db.QueryRowContext(ctx, `SELECT `+certCols+` FROM certificates WHERE name = ?`, name)) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return c, err } // CreateCertificate adds a certificate (ErrConflict when the name is taken). func (s *Store) CreateCertificate(ctx context.Context, c *model.Certificate) error { now := time.Now().UTC() c.CreatedAt, c.UpdatedAt = now, now res, err := s.db.ExecContext(ctx, `INSERT INTO certificates (`+certCols[len("id, "):]+`) VALUES (`+placeholders(28)+`)`, certArgs(c)...) if isUnique(err) { return ErrConflict } if err != nil { return err } c.ID, err = res.LastInsertId() return err } // UpdateCertificate saves every field of c. func (s *Store) UpdateCertificate(ctx context.Context, c *model.Certificate) error { c.UpdatedAt = time.Now().UTC() res, err := s.db.ExecContext(ctx, `UPDATE certificates SET name=?, source=?, domains=?, cert_file=?, key_file=?, uses=?, challenge=?, key_type=?, comment=?, cert_pem=?, key_pem=?, names=?, not_before=?, not_after=?, issuer=?, serial=?, renew_at=?, ari_checked_at=?, last_attempt=?, last_success=?, last_error=?, failures=?, last_routes=?, created_at=?, updated_at=?, created_by=?, updated_by=?, owner_id=? WHERE id=?`, append(certArgs(c), c.ID)...) if isUnique(err) { return ErrConflict } if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return ErrNotFound } return nil } // DeleteCertificate deletes a certificate. func (s *Store) DeleteCertificate(ctx context.Context, id int64) error { res, err := s.db.ExecContext(ctx, `DELETE FROM certificates WHERE id = ?`, id) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return ErrNotFound } return nil } func certArgs(c *model.Certificate) []any { list := func(l []string) string { if l == nil { l = []string{} } return jsonText(l) } return []any{c.Name, c.Source, list(c.Domains), c.CertFile, c.KeyFile, list(c.Uses), c.Challenge, c.KeyType, c.Comment, c.CertPEM, c.KeyPEM, list(c.Names), tsPtr(c.NotBefore), tsPtr(c.NotAfter), c.Issuer, c.Serial, tsPtr(c.RenewAt), tsPtr(c.ARICheckedAt), tsPtr(c.LastAttempt), tsPtr(c.LastSuccess), c.LastError, c.Failures, list(c.LastRoutes), ts(c.CreatedAt), ts(c.UpdatedAt), c.CreatedBy, c.UpdatedBy, c.OwnerID} } // RecordCertificatePull notes a download of a certificate, replacing the // same downloader's earlier one. func (s *Store) RecordCertificatePull(ctx context.Context, p *model.CertificatePull) error { _, err := s.db.ExecContext(ctx, `INSERT INTO certificate_pulls (cert_id, who, user_id, token_id, serial, ip, at) VALUES (?,?,?,?,?,?,?) ON CONFLICT (cert_id, who) DO UPDATE SET user_id = excluded.user_id, token_id = excluded.token_id, serial = excluded.serial, ip = excluded.ip, at = excluded.at`, p.CertID, p.Who, p.UserID, p.TokenID, p.Serial, p.IP, ts(p.At)) return err } // CertificatePulls returns who downloaded each certificate, newest first, // by certificate ID. func (s *Store) CertificatePulls(ctx context.Context) (map[int64][]model.CertificatePull, error) { rows, err := s.db.QueryContext(ctx, `SELECT cert_id, who, user_id, token_id, serial, ip, at FROM certificate_pulls ORDER BY at DESC`) if err != nil { return nil, err } out := map[int64][]model.CertificatePull{} for rows.Next() { var p model.CertificatePull var at string if err := rows.Scan(&p.CertID, &p.Who, &p.UserID, &p.TokenID, &p.Serial, &p.IP, &at); err != nil { rows.Close() return nil, err } p.At = parseTS(at) out[p.CertID] = append(out[p.CertID], p) } return out, closeRows(rows) }