mirror of
https://github.com/1f349/orchid.git
synced 2024-12-21 15:44:08 +00:00
Start updating to support sqlc and migrations
This commit is contained in:
parent
9b3c801ebf
commit
094ac9030a
192
database/certificate.sql.go
Normal file
192
database/certificate.sql.go
Normal file
@ -0,0 +1,192 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// source: certificate.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
const addCertificate = `-- name: AddCertificate :exec
|
||||
INSERT INTO certificates (owner, dns, not_after, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`
|
||||
|
||||
type AddCertificateParams struct {
|
||||
Owner string `json:"owner"`
|
||||
Dns sql.NullInt64 `json:"dns"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddCertificate(ctx context.Context, arg AddCertificateParams) error {
|
||||
_, err := q.db.ExecContext(ctx, addCertificate,
|
||||
arg.Owner,
|
||||
arg.Dns,
|
||||
arg.NotAfter,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const checkCertOwner = `-- name: CheckCertOwner :one
|
||||
SELECT id, owner
|
||||
FROM certificates
|
||||
WHERE active = 1
|
||||
and id = ?
|
||||
`
|
||||
|
||||
type CheckCertOwnerRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
}
|
||||
|
||||
func (q *Queries) CheckCertOwner(ctx context.Context, id int64) (CheckCertOwnerRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, checkCertOwner, id)
|
||||
var i CheckCertOwnerRow
|
||||
err := row.Scan(&i.ID, &i.Owner)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const findNextCert = `-- name: FindNextCert :one
|
||||
SELECT cert.id, cert.not_after, dns_acme.type, dns_acme.token, cert.temp_parent
|
||||
FROM certificates AS cert
|
||||
LEFT OUTER JOIN dns_acme ON cert.dns = dns_acme.id
|
||||
WHERE cert.active = 1
|
||||
AND (cert.auto_renew = 1 OR cert.not_after IS NULL)
|
||||
AND cert.renewing = 0
|
||||
AND cert.renew_failed = 0
|
||||
AND (cert.not_after IS NULL OR DATETIME(cert.not_after, 'utc', '-30 days') < DATETIME())
|
||||
ORDER BY cert.temp_parent, cert.not_after DESC NULLS FIRST
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type FindNextCertRow struct {
|
||||
ID int64 `json:"id"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
Type sql.NullString `json:"type"`
|
||||
Token sql.NullString `json:"token"`
|
||||
TempParent sql.NullInt64 `json:"temp_parent"`
|
||||
}
|
||||
|
||||
func (q *Queries) FindNextCert(ctx context.Context) (FindNextCertRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, findNextCert)
|
||||
var i FindNextCertRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.NotAfter,
|
||||
&i.Type,
|
||||
&i.Token,
|
||||
&i.TempParent,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const findOwnedCerts = `-- name: FindOwnedCerts :many
|
||||
SELECT cert.id,
|
||||
cert.auto_renew,
|
||||
cert.active,
|
||||
cert.renewing,
|
||||
cert.renew_failed,
|
||||
cert.not_after,
|
||||
cert.updated_at,
|
||||
certificate_domains.domain
|
||||
FROM certificates AS cert
|
||||
INNER JOIN certificate_domains ON cert.id = certificate_domains.cert_id
|
||||
`
|
||||
|
||||
type FindOwnedCertsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
Active bool `json:"active"`
|
||||
Renewing bool `json:"renewing"`
|
||||
RenewFailed bool `json:"renew_failed"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
func (q *Queries) FindOwnedCerts(ctx context.Context) ([]FindOwnedCertsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, findOwnedCerts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []FindOwnedCertsRow
|
||||
for rows.Next() {
|
||||
var i FindOwnedCertsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.AutoRenew,
|
||||
&i.Active,
|
||||
&i.Renewing,
|
||||
&i.RenewFailed,
|
||||
&i.NotAfter,
|
||||
&i.UpdatedAt,
|
||||
&i.Domain,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const removeCertificate = `-- name: RemoveCertificate :exec
|
||||
UPDATE certificates
|
||||
SET active = 0
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) RemoveCertificate(ctx context.Context, id int64) error {
|
||||
_, err := q.db.ExecContext(ctx, removeCertificate, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateCertAfterRenewal = `-- name: UpdateCertAfterRenewal :exec
|
||||
UPDATE certificates
|
||||
SET renewing = 0,
|
||||
renew_failed=0,
|
||||
not_after=?,
|
||||
updated_at=?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateCertAfterRenewalParams struct {
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateCertAfterRenewal(ctx context.Context, arg UpdateCertAfterRenewalParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateCertAfterRenewal, arg.NotAfter, arg.UpdatedAt, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateRenewingState = `-- name: UpdateRenewingState :exec
|
||||
UPDATE certificates
|
||||
SET renewing = ?,
|
||||
renew_failed = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateRenewingStateParams struct {
|
||||
Renewing bool `json:"renewing"`
|
||||
RenewFailed bool `json:"renew_failed"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateRenewingState(ctx context.Context, arg UpdateRenewingStateParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateRenewingState, arg.Renewing, arg.RenewFailed, arg.ID)
|
||||
return err
|
||||
}
|
121
database/certificate_domains.sql.go
Normal file
121
database/certificate_domains.sql.go
Normal file
@ -0,0 +1,121 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// source: certificate_domains.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const addDomains = `-- name: AddDomains :exec
|
||||
INSERT INTO certificate_domains (cert_id, domain, state)
|
||||
VALUES (?, ?, ?)
|
||||
`
|
||||
|
||||
type AddDomainsParams struct {
|
||||
CertID int64 `json:"cert_id"`
|
||||
Domain string `json:"domain"`
|
||||
State int64 `json:"state"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddDomains(ctx context.Context, arg AddDomainsParams) error {
|
||||
_, err := q.db.ExecContext(ctx, addDomains, arg.CertID, arg.Domain, arg.State)
|
||||
return err
|
||||
}
|
||||
|
||||
const getDomainStatesForCert = `-- name: GetDomainStatesForCert :many
|
||||
SELECT domain, state
|
||||
FROM certificate_domains
|
||||
WHERE cert_id = ?
|
||||
`
|
||||
|
||||
type GetDomainStatesForCertRow struct {
|
||||
Domain string `json:"domain"`
|
||||
State int64 `json:"state"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetDomainStatesForCert(ctx context.Context, certID int64) ([]GetDomainStatesForCertRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getDomainStatesForCert, certID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetDomainStatesForCertRow
|
||||
for rows.Next() {
|
||||
var i GetDomainStatesForCertRow
|
||||
if err := rows.Scan(&i.Domain, &i.State); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getDomainsForCertificate = `-- name: GetDomainsForCertificate :many
|
||||
SELECT domain
|
||||
FROM certificate_domains
|
||||
WHERE cert_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetDomainsForCertificate(ctx context.Context, certID int64) ([]string, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getDomainsForCertificate, certID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []string
|
||||
for rows.Next() {
|
||||
var domain string
|
||||
if err := rows.Scan(&domain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, domain)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const setDomainStateForCert = `-- name: SetDomainStateForCert :exec
|
||||
UPDATE certificate_domains
|
||||
SET state = ?
|
||||
WHERE cert_id = ?
|
||||
`
|
||||
|
||||
type SetDomainStateForCertParams struct {
|
||||
State int64 `json:"state"`
|
||||
CertID int64 `json:"cert_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) SetDomainStateForCert(ctx context.Context, arg SetDomainStateForCertParams) error {
|
||||
_, err := q.db.ExecContext(ctx, setDomainStateForCert, arg.State, arg.CertID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateDomains = `-- name: UpdateDomains :exec
|
||||
UPDATE certificate_domains
|
||||
SET state = ?
|
||||
WHERE domain IN ?
|
||||
`
|
||||
|
||||
type UpdateDomainsParams struct {
|
||||
State int64 `json:"state"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateDomains(ctx context.Context, arg UpdateDomainsParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateDomains, arg.State, arg.Domain)
|
||||
return err
|
||||
}
|
31
database/db.go
Normal file
31
database/db.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
0
database/migrations/20240308160822_init.down.sql
Normal file
0
database/migrations/20240308160822_init.down.sql
Normal file
33
database/migrations/20240308160822_init.up.sql
Normal file
33
database/migrations/20240308160822_init.up.sql
Normal file
@ -0,0 +1,33 @@
|
||||
CREATE TABLE IF NOT EXISTS certificates
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner VARCHAR NOT NULL,
|
||||
dns INTEGER,
|
||||
auto_renew BOOLEAN NOT NULL DEFAULT 0,
|
||||
active BOOLEAN NOT NULL DEFAULT 0,
|
||||
renewing BOOLEAN NOT NULL DEFAULT 0,
|
||||
renew_failed BOOLEAN NOT NULL DEFAULT 0,
|
||||
not_after DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
temp_parent INTEGER,
|
||||
FOREIGN KEY (dns) REFERENCES dns_acme (id),
|
||||
FOREIGN KEY (temp_parent) REFERENCES certificates (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS certificate_domains
|
||||
(
|
||||
domain_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cert_id INTEGER NOT NULL,
|
||||
domain VARCHAR NOT NULL,
|
||||
state INTEGER NOT NULL DEFAULT 1,
|
||||
UNIQUE (cert_id, domain),
|
||||
FOREIGN KEY (cert_id) REFERENCES certificates (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dns_acme
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type VARCHAR NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
token VARCHAR NOT NULL
|
||||
);
|
37
database/models.go
Normal file
37
database/models.go
Normal file
@ -0,0 +1,37 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Certificate struct {
|
||||
ID int64 `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
Dns sql.NullInt64 `json:"dns"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
Active bool `json:"active"`
|
||||
Renewing bool `json:"renewing"`
|
||||
RenewFailed bool `json:"renew_failed"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
TempParent sql.NullInt64 `json:"temp_parent"`
|
||||
}
|
||||
|
||||
type CertificateDomain struct {
|
||||
DomainID int64 `json:"domain_id"`
|
||||
CertID int64 `json:"cert_id"`
|
||||
Domain string `json:"domain"`
|
||||
State int64 `json:"state"`
|
||||
}
|
||||
|
||||
type DnsAcme struct {
|
||||
ID int64 `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Email string `json:"email"`
|
||||
Token string `json:"token"`
|
||||
}
|
52
database/queries/certificate.sql
Normal file
52
database/queries/certificate.sql
Normal file
@ -0,0 +1,52 @@
|
||||
-- name: FindNextCert :one
|
||||
SELECT cert.id, cert.not_after, dns_acme.type, dns_acme.token, cert.temp_parent
|
||||
FROM certificates AS cert
|
||||
LEFT OUTER JOIN dns_acme ON cert.dns = dns_acme.id
|
||||
WHERE cert.active = 1
|
||||
AND (cert.auto_renew = 1 OR cert.not_after IS NULL)
|
||||
AND cert.renewing = 0
|
||||
AND cert.renew_failed = 0
|
||||
AND (cert.not_after IS NULL OR DATETIME(cert.not_after, 'utc', '-30 days') < DATETIME())
|
||||
ORDER BY cert.temp_parent, cert.not_after DESC NULLS FIRST
|
||||
LIMIT 1;
|
||||
|
||||
-- name: FindOwnedCerts :many
|
||||
SELECT cert.id,
|
||||
cert.auto_renew,
|
||||
cert.active,
|
||||
cert.renewing,
|
||||
cert.renew_failed,
|
||||
cert.not_after,
|
||||
cert.updated_at,
|
||||
certificate_domains.domain
|
||||
FROM certificates AS cert
|
||||
INNER JOIN certificate_domains ON cert.id = certificate_domains.cert_id;
|
||||
|
||||
-- name: UpdateRenewingState :exec
|
||||
UPDATE certificates
|
||||
SET renewing = ?,
|
||||
renew_failed = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: UpdateCertAfterRenewal :exec
|
||||
UPDATE certificates
|
||||
SET renewing = 0,
|
||||
renew_failed=0,
|
||||
not_after=?,
|
||||
updated_at=?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: AddCertificate :exec
|
||||
INSERT INTO certificates (owner, dns, not_after, updated_at)
|
||||
VALUES (?, ?, ?, ?);
|
||||
|
||||
-- name: RemoveCertificate :exec
|
||||
UPDATE certificates
|
||||
SET active = 0
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: CheckCertOwner :one
|
||||
SELECT id, owner
|
||||
FROM certificates
|
||||
WHERE active = 1
|
||||
and id = ?;
|
23
database/queries/certificate_domains.sql
Normal file
23
database/queries/certificate_domains.sql
Normal file
@ -0,0 +1,23 @@
|
||||
-- name: GetDomainsForCertificate :many
|
||||
SELECT domain
|
||||
FROM certificate_domains
|
||||
WHERE cert_id = ?;
|
||||
|
||||
-- name: GetDomainStatesForCert :many
|
||||
SELECT domain, state
|
||||
FROM certificate_domains
|
||||
WHERE cert_id = ?;
|
||||
|
||||
-- name: SetDomainStateForCert :exec
|
||||
UPDATE certificate_domains
|
||||
SET state = ?
|
||||
WHERE cert_id = ?;
|
||||
|
||||
-- name: AddDomains :exec
|
||||
INSERT INTO certificate_domains (cert_id, domain, state)
|
||||
VALUES (?, ?, ?);
|
||||
|
||||
-- name: UpdateDomains :exec
|
||||
UPDATE certificate_domains
|
||||
SET state = ?
|
||||
WHERE domain IN (sqlc.slice("domains"));
|
23
database/tx.go
Normal file
23
database/tx.go
Normal file
@ -0,0 +1,23 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func (q *Queries) UseTx(ctx context.Context, cb func(tx *Queries) error) error {
|
||||
sqlDB, ok := q.db.(*sql.DB)
|
||||
if !ok {
|
||||
panic("cannot open transaction without sql.DB")
|
||||
}
|
||||
tx, err := sqlDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
err = cb(q.WithTx(tx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
8
go.mod
8
go.mod
@ -9,6 +9,7 @@ require (
|
||||
github.com/MrMelon54/certgen v0.0.1
|
||||
github.com/MrMelon54/exit-reload v0.0.1
|
||||
github.com/go-acme/lego/v4 v4.14.2
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||
github.com/google/subcommands v1.2.0
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
@ -25,6 +26,8 @@ require (
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@ -32,9 +35,10 @@ require (
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
golang.org/x/crypto v0.20.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/term v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
|
18
go.sum
18
go.sum
@ -25,6 +25,8 @@ github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7
|
||||
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@ -35,6 +37,11 @@ github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
@ -45,6 +52,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@ -74,10 +83,13 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
|
||||
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
@ -87,8 +99,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
38
initdb.go
Normal file
38
initdb.go
Normal file
@ -0,0 +1,38 @@
|
||||
package orchid
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"github.com/1f349/orchid/database"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
)
|
||||
|
||||
//go:embed database/migrations/*.sql
|
||||
var migrations embed.FS
|
||||
|
||||
func InitDB(p string) (*database.Queries, error) {
|
||||
migDrv, err := iofs.New(migrations, "database/migrations")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbOpen, err := sql.Open("sqlite3", p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbDrv, err := sqlite3.WithInstance(dbOpen, &sqlite3.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mig, err := migrate.NewWithInstance("iofs", migDrv, "sqlite3", dbDrv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = mig.Up()
|
||||
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return nil, err
|
||||
}
|
||||
return database.New(dbOpen), nil
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS certificates
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner VARCHAR,
|
||||
dns INTEGER,
|
||||
auto_renew INTEGER DEFAULT 0,
|
||||
active INTEGER DEFAULT 0,
|
||||
renewing INTEGER DEFAULT 0,
|
||||
renew_failed INTEGER DEFAULT 0,
|
||||
not_after DATETIME,
|
||||
updated_at DATETIME,
|
||||
temp_parent INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (dns) REFERENCES dns_acme (id),
|
||||
FOREIGN KEY (temp_parent) REFERENCES certificates (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS certificate_domains
|
||||
(
|
||||
domain_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cert_id INTEGER,
|
||||
domain VARCHAR,
|
||||
state INTEGER DEFAULT 1,
|
||||
UNIQUE (cert_id, domain),
|
||||
FOREIGN KEY (cert_id) REFERENCES certificates (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dns_acme
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type VARCHAR,
|
||||
email VARCHAR,
|
||||
token VARCHAR
|
||||
);
|
@ -1,9 +0,0 @@
|
||||
select cert.id, cert.not_after, dns_acme.type, dns_acme.token, cert.temp_parent
|
||||
from certificates as cert
|
||||
left outer join dns_acme on cert.dns = dns_acme.id
|
||||
where cert.active = 1
|
||||
and (cert.auto_renew = 1 or cert.not_after IS NULL)
|
||||
and cert.renewing = 0
|
||||
and cert.renew_failed = 0
|
||||
and (cert.not_after IS NULL or DATETIME(cert.not_after, 'utc', '-30 days') < DATETIME())
|
||||
order by cert.temp_parent, cert.not_after DESC NULLS FIRST
|
@ -2,16 +2,17 @@ package renewal
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Contains local types for the renewal service
|
||||
type localCertData struct {
|
||||
id uint64
|
||||
id int64
|
||||
dns struct {
|
||||
name sql.NullString
|
||||
token sql.NullString
|
||||
}
|
||||
notAfter sql.NullTime
|
||||
notAfter time.Time
|
||||
domains []string
|
||||
tempParent uint64
|
||||
tempParent sql.NullInt64
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package renewal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
@ -10,6 +11,7 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/1f349/orchid/database"
|
||||
"github.com/1f349/orchid/pebble"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
@ -18,7 +20,6 @@ import (
|
||||
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
@ -28,13 +29,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnsupportedDNSProvider = errors.New("unsupported DNS provider")
|
||||
//go:embed find-next-cert.sql
|
||||
findNextCertSql string
|
||||
//go:embed create-tables.sql
|
||||
createTableCertificates string
|
||||
)
|
||||
var ErrUnsupportedDNSProvider = errors.New("unsupported DNS provider")
|
||||
|
||||
const (
|
||||
DomainStateNormal = 0
|
||||
@ -57,7 +52,7 @@ var testDnsOptions interface {
|
||||
// `_acme-challenges` TXT records are updated to validate the ownership of the
|
||||
// specified domains.
|
||||
type Service struct {
|
||||
db *sql.DB
|
||||
db *database.Queries
|
||||
httpAcme challenge.Provider
|
||||
certTicker *time.Ticker
|
||||
certDone chan struct{}
|
||||
@ -73,7 +68,7 @@ type Service struct {
|
||||
}
|
||||
|
||||
// NewService creates a new certificate renewal service.
|
||||
func NewService(wg *sync.WaitGroup, db *sql.DB, httpAcme challenge.Provider, leConfig LetsEncryptConfig, certDir, keyDir string) (*Service, error) {
|
||||
func NewService(wg *sync.WaitGroup, db *database.Queries, httpAcme challenge.Provider, leConfig LetsEncryptConfig, certDir, keyDir string) (*Service, error) {
|
||||
s := &Service{
|
||||
db: db,
|
||||
httpAcme: httpAcme,
|
||||
@ -104,12 +99,6 @@ func NewService(wg *sync.WaitGroup, db *sql.DB, httpAcme challenge.Provider, leC
|
||||
return nil, fmt.Errorf("failed to resolve LetsEncrypt account private key: %w", err)
|
||||
}
|
||||
|
||||
// init domains table
|
||||
_, err = s.db.Exec(createTableCertificates)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create certificates table: %w", err)
|
||||
}
|
||||
|
||||
// resolve CA information
|
||||
s.resolveCADirectory(leConfig.Directory)
|
||||
err = s.resolveCACertificate(leConfig.Certificate)
|
||||
@ -286,50 +275,30 @@ func (s *Service) findNextCertificateToRenew() (*localCertData, error) {
|
||||
d := &localCertData{}
|
||||
|
||||
// sql or something, the query is in `find-next-cert.sql`
|
||||
row, err := s.db.Query(findNextCertSql)
|
||||
row, err := s.db.FindNextCert(context.Background())
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to run query: %w", err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
// if next fails no rows were found
|
||||
if !row.Next() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// scan the first row
|
||||
err = row.Scan(&d.id, &d.notAfter, &d.dns.name, &d.dns.token, &d.tempParent)
|
||||
switch err {
|
||||
case nil:
|
||||
// no nothing
|
||||
break
|
||||
case io.EOF:
|
||||
// no certificate to update
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("failed to scan table row: %w", err)
|
||||
}
|
||||
d.id = row.ID
|
||||
d.dns.name = row.Type
|
||||
d.dns.token = row.Token
|
||||
d.notAfter = row.NotAfter
|
||||
d.tempParent = row.TempParent
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (s *Service) fetchDomains(localData *localCertData) ([]string, error) {
|
||||
// more sql: this one just grabs all the domains for a certificate
|
||||
query, err := s.db.Query(`SELECT domain FROM certificate_domains WHERE cert_id = ?`, resolveTempParent(localData))
|
||||
domains, err := s.db.GetDomainsForCertificate(context.Background(), resolveTempParent(localData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch domains for certificate: %d: %w", localData.id, err)
|
||||
}
|
||||
|
||||
// convert query responses to a string slice
|
||||
domains := make([]string, 0)
|
||||
for query.Next() {
|
||||
var domain string
|
||||
err := query.Scan(&domain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan row from domains table: %d: %w", localData.id, err)
|
||||
}
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
// if no domains were found then the renewal will fail
|
||||
if len(domains) == 0 {
|
||||
return nil, fmt.Errorf("no domains registered for certificate: %d", localData.id)
|
||||
@ -391,7 +360,7 @@ func (s *Service) getDnsProvider(name, token string) (challenge.Provider, error)
|
||||
|
||||
// getPrivateKey reads the private key for the specified certificate id, or
|
||||
// generates one is the file doesn't exist
|
||||
func (s *Service) getPrivateKey(id uint64) (*rsa.PrivateKey, error) {
|
||||
func (s *Service) getPrivateKey(id int64) (*rsa.PrivateKey, error) {
|
||||
fPath := filepath.Join(s.keyDir, fmt.Sprintf("%d.key.pem", id))
|
||||
pemBytes, err := os.ReadFile(fPath)
|
||||
if err != nil {
|
||||
@ -433,13 +402,20 @@ func (s *Service) renewCert(localData *localCertData) error {
|
||||
}
|
||||
|
||||
// set the NotAfter/NotBefore in the database
|
||||
_, err = s.db.Exec(`UPDATE certificates SET renewing = 0, renew_failed = 0, not_after = ?, updated_at = ? WHERE id = ?`, cert.NotAfter, cert.NotBefore, localData.id)
|
||||
err = s.db.UpdateCertAfterRenewal(context.Background(), database.UpdateCertAfterRenewalParams{
|
||||
NotAfter: cert.NotAfter,
|
||||
UpdatedAt: cert.NotBefore,
|
||||
ID: localData.id,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update cert %d in database: %w", localData.id, err)
|
||||
}
|
||||
|
||||
// set domains to normal state
|
||||
_, err = s.db.Exec(`UPDATE certificate_domains SET state = ? WHERE cert_id = ?`, DomainStateNormal, localData.id)
|
||||
err = s.db.SetDomainStateForCert(context.Background(), database.SetDomainStateForCertParams{
|
||||
State: DomainStateNormal,
|
||||
CertID: localData.id,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update domains for %d in database: %w", localData.id, err)
|
||||
}
|
||||
@ -517,8 +493,12 @@ func (s *Service) renewCertInternal(localData *localCertData) (*x509.Certificate
|
||||
|
||||
// setRenewing sets the renewing and failed states in the database for a
|
||||
// specified certificate id.
|
||||
func (s *Service) setRenewing(id uint64, renewing, failed bool) {
|
||||
_, err := s.db.Exec("UPDATE certificates SET renewing = ?, renew_failed = ? WHERE id = ?", renewing, failed, id)
|
||||
func (s *Service) setRenewing(id int64, renewing, failed bool) {
|
||||
err := s.db.UpdateRenewingState(context.Background(), database.UpdateRenewingStateParams{
|
||||
Renewing: renewing,
|
||||
RenewFailed: failed,
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[Renewal] Failed to set renewing/failed mode in database %d: %s\n", id, err)
|
||||
}
|
||||
@ -526,7 +506,7 @@ func (s *Service) setRenewing(id uint64, renewing, failed bool) {
|
||||
|
||||
// writeCertFile writes the output certificate file and renames the current one
|
||||
// to include `-old` in the name.
|
||||
func (s *Service) writeCertFile(id uint64, certBytes []byte) error {
|
||||
func (s *Service) writeCertFile(id int64, certBytes []byte) error {
|
||||
oldPath := filepath.Join(s.certDir, fmt.Sprintf("%d-old.cert.pem", id))
|
||||
newPath := filepath.Join(s.certDir, fmt.Sprintf("%d.cert.pem", id))
|
||||
|
||||
@ -552,9 +532,9 @@ func (s *Service) writeCertFile(id uint64, certBytes []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveTempParent(local *localCertData) uint64 {
|
||||
if local.tempParent > 0 {
|
||||
return local.tempParent
|
||||
func resolveTempParent(local *localCertData) int64 {
|
||||
if local.tempParent.Valid {
|
||||
return local.tempParent.Int64
|
||||
}
|
||||
return local.id
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/mjwt/claims"
|
||||
"github.com/1f349/orchid/database"
|
||||
oUtils "github.com/1f349/orchid/utils"
|
||||
vUtils "github.com/1f349/violet/utils"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
@ -22,7 +24,7 @@ type DomainStateValue struct {
|
||||
}
|
||||
|
||||
type Certificate struct {
|
||||
Id int `json:"id"`
|
||||
Id int64 `json:"id"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
Active bool `json:"active"`
|
||||
Renewing bool `json:"renewing"`
|
||||
@ -32,14 +34,11 @@ type Certificate struct {
|
||||
Domains []string `json:"domains"`
|
||||
}
|
||||
|
||||
//go:embed find-owned-certs.sql
|
||||
var findOwnedCerts string
|
||||
|
||||
// NewApiServer creates and runs a http server containing all the API
|
||||
// endpoints for the software
|
||||
//
|
||||
// `/cert` - edit certificate
|
||||
func NewApiServer(listen string, db *sql.DB, signer mjwt.Verifier, domains oUtils.DomainChecker) *http.Server {
|
||||
func NewApiServer(listen string, db *database.Queries, signer mjwt.Verifier, domains oUtils.DomainChecker) *http.Server {
|
||||
r := httprouter.New()
|
||||
|
||||
r.GET("/", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
@ -55,25 +54,28 @@ func NewApiServer(listen string, db *sql.DB, signer mjwt.Verifier, domains oUtil
|
||||
}
|
||||
|
||||
// query database
|
||||
query, err := db.Query(findOwnedCerts)
|
||||
rows, err := db.FindOwnedCerts(context.Background())
|
||||
if err != nil {
|
||||
log.Println("Failed after reading certificates from database: ", err)
|
||||
http.Error(rw, "Database Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
mOther := make(map[int]*Certificate) // other certificates
|
||||
m := make(map[int]*Certificate) // certificates owned by this user
|
||||
mOther := make(map[int64]*Certificate) // other certificates
|
||||
m := make(map[int64]*Certificate) // certificates owned by this user
|
||||
|
||||
// loop over query rows
|
||||
for query.Next() {
|
||||
var c Certificate
|
||||
var d string
|
||||
err := query.Scan(&c.Id, &c.AutoRenew, &c.Active, &c.Renewing, &c.RenewFailed, &c.NotAfter, &c.UpdatedAt, &d)
|
||||
if err != nil {
|
||||
log.Println("Failed to read certificate from database: ", err)
|
||||
http.Error(rw, "Database Error", http.StatusInternalServerError)
|
||||
return
|
||||
for _, row := range rows {
|
||||
c := Certificate{
|
||||
Id: row.ID,
|
||||
AutoRenew: row.AutoRenew,
|
||||
Active: row.Active,
|
||||
Renewing: row.Renewing,
|
||||
RenewFailed: row.RenewFailed,
|
||||
NotAfter: row.NotAfter,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
d := row.Domain
|
||||
|
||||
// check in owned map
|
||||
if cert, ok := m[c.Id]; ok {
|
||||
@ -105,11 +107,6 @@ func NewApiServer(listen string, db *sql.DB, signer mjwt.Verifier, domains oUtil
|
||||
m[c.Id] = &c
|
||||
}
|
||||
}
|
||||
if err := query.Err(); err != nil {
|
||||
log.Println("Failed after reading certificates from database: ", err)
|
||||
http.Error(rw, "Database Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(rw).Encode(m)
|
||||
}))
|
||||
@ -124,15 +121,20 @@ func NewApiServer(listen string, db *sql.DB, signer mjwt.Verifier, domains oUtil
|
||||
}))
|
||||
|
||||
r.POST("/cert", checkAuthWithPerm(signer, "orchid:cert", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
|
||||
_, err := db.Exec(`INSERT INTO certificates (owner, dns, updated_at) VALUES (?, ?, ?)`, b.Subject, 0, time.Now())
|
||||
err := db.AddCertificate(req.Context(), database.AddCertificateParams{
|
||||
Owner: b.Subject,
|
||||
Dns: sql.NullInt64{},
|
||||
NotAfter: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to delete certificate")
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
}))
|
||||
r.DELETE("/cert/:id", checkAuthForCertificate(signer, "orchid:cert", db, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId uint64) {
|
||||
_, err := db.Exec(`UPDATE certificates SET active = 0 WHERE id = ?`, certId)
|
||||
r.DELETE("/cert/:id", checkAuthForCertificate(signer, "orchid:cert", db, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId int64) {
|
||||
err := db.RemoveCertificate(req.Context(), certId)
|
||||
if err != nil {
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to delete certificate")
|
||||
return
|
||||
@ -194,7 +196,7 @@ func apiError(rw http.ResponseWriter, code int, m string) {
|
||||
|
||||
// lookupCertOwner finds the certificate matching the id string and returns the
|
||||
// numeric id, owner and possible error, only works for active certificates.
|
||||
func checkCertOwner(db *sql.DB, idStr string, b AuthClaims) (uint64, error) {
|
||||
func checkCertOwner(db *database.Queries, idStr string, b AuthClaims) (int64, error) {
|
||||
// parse the id
|
||||
rawId, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
@ -202,54 +204,18 @@ func checkCertOwner(db *sql.DB, idStr string, b AuthClaims) (uint64, error) {
|
||||
}
|
||||
|
||||
// run database query
|
||||
row := db.QueryRow(`SELECT id, owner FROM certificates WHERE active = 1 and id = ?`, rawId)
|
||||
|
||||
// scan in result values
|
||||
var id uint64
|
||||
var owner string
|
||||
err = row.Scan(&id, &owner)
|
||||
row, err := db.CheckCertOwner(context.Background(), int64(rawId))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("scan error: %w", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// check the owner is the mjwt token subject
|
||||
if b.Subject != owner {
|
||||
return id, fmt.Errorf("not the certificate owner")
|
||||
if b.Subject != row.Owner {
|
||||
return row.ID, fmt.Errorf("not the certificate owner")
|
||||
}
|
||||
|
||||
// it's all valid, return the values
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// safeTransaction completes a database transaction safely allowing for rollbacks
|
||||
// if the callback errors
|
||||
func safeTransaction(rw http.ResponseWriter, db *sql.DB, cb func(rw http.ResponseWriter, tx *sql.Tx) error) error {
|
||||
// start a transaction
|
||||
begin, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin a transaction")
|
||||
}
|
||||
|
||||
// init defer rollback
|
||||
needsRollback := true
|
||||
defer func() {
|
||||
if needsRollback {
|
||||
_ = begin.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// run main code within the transaction session
|
||||
err = cb(rw, begin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// clear the rollback flag and commit the transaction
|
||||
needsRollback = false
|
||||
if begin.Commit() != nil {
|
||||
return fmt.Errorf("failed to commit a transaction")
|
||||
}
|
||||
return nil
|
||||
return row.ID, nil
|
||||
}
|
||||
|
||||
// getDomainOwnershipClaims returns the domains marked as owned from PermStorage,
|
||||
|
@ -1,9 +1,9 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/mjwt/auth"
|
||||
"github.com/1f349/orchid/database"
|
||||
vUtils "github.com/1f349/violet/utils"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"log"
|
||||
@ -14,7 +14,7 @@ type AuthClaims mjwt.BaseTypeClaims[auth.AccessTokenClaims]
|
||||
|
||||
type AuthCallback func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims)
|
||||
|
||||
type CertAuthCallback func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId uint64)
|
||||
type CertAuthCallback func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId int64)
|
||||
|
||||
// checkAuth validates the bearer token against a mjwt.Verifier and returns an
|
||||
// error message or continues to the next handler
|
||||
@ -53,7 +53,7 @@ func checkAuthWithPerm(verify mjwt.Verifier, perm string, cb AuthCallback) httpr
|
||||
}
|
||||
|
||||
// checkAuthForCertificate
|
||||
func checkAuthForCertificate(verify mjwt.Verifier, perm string, db *sql.DB, cb CertAuthCallback) httprouter.Handle {
|
||||
func checkAuthForCertificate(verify mjwt.Verifier, perm string, db *database.Queries, cb CertAuthCallback) httprouter.Handle {
|
||||
return checkAuthWithPerm(verify, perm, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
|
||||
// lookup certificate owner
|
||||
id, err := checkCertOwner(db, params.ByName("id"), b)
|
||||
|
@ -1,48 +1,37 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/orchid/database"
|
||||
"github.com/1f349/orchid/renewal"
|
||||
"github.com/1f349/orchid/utils"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func certDomainManageGET(db *sql.DB, signer mjwt.Verifier) httprouter.Handle {
|
||||
func certDomainManageGET(db *database.Queries, signer mjwt.Verifier) httprouter.Handle {
|
||||
return checkAuthForCertificate(signer, "orchid:cert:edit", db, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId uint64) {
|
||||
query, err := db.Query(`SELECT domain, state FROM certificate_domains WHERE cert_id = ?`, certId)
|
||||
rows, err := db.GetDomainStatesForCert(context.Background(), int64(certId))
|
||||
if err != nil {
|
||||
apiError(rw, http.StatusInsufficientStorage, "Database error")
|
||||
return
|
||||
}
|
||||
|
||||
// collect all the domains and state values
|
||||
var domainStates []DomainStateValue
|
||||
for query.Next() {
|
||||
var a DomainStateValue
|
||||
err := query.Scan(&a.Domain, &a.State)
|
||||
if err != nil {
|
||||
apiError(rw, http.StatusInsufficientStorage, "Database error")
|
||||
return
|
||||
}
|
||||
domainStates = append(domainStates, a)
|
||||
}
|
||||
|
||||
// write output
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
m := map[string]any{
|
||||
"id": fmt.Sprintf("%d", certId),
|
||||
"domains": domainStates,
|
||||
"domains": rows,
|
||||
}
|
||||
_ = json.NewEncoder(rw).Encode(m)
|
||||
})
|
||||
}
|
||||
|
||||
func certDomainManagePUTandDELETE(db *sql.DB, signer mjwt.Verifier, domains utils.DomainChecker) httprouter.Handle {
|
||||
return checkAuthForCertificate(signer, "orchid:cert:edit", db, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId uint64) {
|
||||
func certDomainManagePUTandDELETE(db *database.Queries, signer mjwt.Verifier, domains utils.DomainChecker) httprouter.Handle {
|
||||
return checkAuthForCertificate(signer, "orchid:cert:edit", db, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId int64) {
|
||||
// check request type
|
||||
isAdd := req.Method == http.MethodPut
|
||||
|
||||
@ -66,18 +55,25 @@ func certDomainManagePUTandDELETE(db *sql.DB, signer mjwt.Verifier, domains util
|
||||
}
|
||||
|
||||
// run a safe transaction to insert or update the certificate domains
|
||||
if safeTransaction(rw, db, func(rw http.ResponseWriter, tx *sql.Tx) error {
|
||||
if db.UseTx(req.Context(), func(tx *database.Queries) error {
|
||||
if isAdd {
|
||||
// insert domains to add
|
||||
for _, i := range d {
|
||||
_, err := tx.Exec(`INSERT INTO certificate_domains (cert_id, domain, state) VALUES (?, ?, ?)`, certId, i, renewal.DomainStateAdded)
|
||||
err := tx.AddDomains(req.Context(), database.AddDomainsParams{
|
||||
CertID: certId,
|
||||
Domain: i,
|
||||
State: renewal.DomainStateAdded,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add domains to the database")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// update domains to removed state
|
||||
_, err := tx.Exec(`UPDATE certificate_domains SET state = ? WHERE domain IN ?`, renewal.DomainStateRemoved, d)
|
||||
err := tx.UpdateDomains(req.Context(), database.UpdateDomainsParams{
|
||||
State: renewal.DomainStateRemoved,
|
||||
Domain: d,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove domains from the database")
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
select cert.id, cert.auto_renew, cert.active, cert.renewing, cert.renew_failed, cert.not_after, cert.updated_at, certificate_domains.domain
|
||||
from certificates as cert
|
||||
inner join certificate_domains on cert.id = certificate_domains.cert_id
|
Loading…
Reference in New Issue
Block a user