Initial
This commit is contained in:
10
internal/storage/migrations.go
Normal file
10
internal/storage/migrations.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package storage
|
||||
|
||||
const SchemaSQL = `
|
||||
CREATE TABLE IF NOT EXISTS status_lists (
|
||||
status_id TEXT PRIMARY KEY,
|
||||
bit_length INTEGER NOT NULL,
|
||||
data_encrypted BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
`
|
||||
167
internal/storage/postgres.go
Normal file
167
internal/storage/postgres.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"netisjwt/internal/statuslist"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("status list not found")
|
||||
|
||||
type PostgresStore struct {
|
||||
db *sql.DB
|
||||
gc cipher.AEAD
|
||||
}
|
||||
|
||||
// NewPostgresStore ustvari repozitorij s pripravljeno AES-GCM sifro.
|
||||
func NewPostgresStore(db *sql.DB, aesKeyBase64 string) (*PostgresStore, error) {
|
||||
key, err := base64.StdEncoding.DecodeString(aesKeyBase64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid STATUS_AES_KEY_B64: %w", err)
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid AES key: %w", err)
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PostgresStore{db: db, gc: gcm}, nil
|
||||
}
|
||||
|
||||
// PoveziInMigriraj odpre povezavo na bazo in izvede schema migracijo.
|
||||
func PoveziInMigriraj(ctx context.Context, dsn string, aesKeyBase64 string) (*PostgresStore, error) {
|
||||
db, err := sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, SchemaSQL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewPostgresStore(db, aesKeyBase64)
|
||||
}
|
||||
|
||||
// Zapri zapre povezavo na podatkovno bazo.
|
||||
func (s *PostgresStore) Zapri() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
// Ustvari shrani novo status listo.
|
||||
func (s *PostgresStore) Ustvari(ctx context.Context, statusID string, list *statuslist.StatusList) error {
|
||||
encrypted, err := s.sifriraj(list.Bajti())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.db.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO status_lists(status_id, bit_length, data_encrypted) VALUES ($1, $2, $3)`,
|
||||
statusID,
|
||||
list.DolzinaBitov(),
|
||||
encrypted,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// SeznamIDjev vrne vse shranjene id-je seznamov.
|
||||
func (s *PostgresStore) SeznamIDjev(ctx context.Context) ([]string, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT status_id FROM status_lists ORDER BY created_at ASC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, id)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Dobi prebere status listo po statusID.
|
||||
func (s *PostgresStore) Dobi(ctx context.Context, statusID string) (*statuslist.StatusList, error) {
|
||||
var bitLength int
|
||||
var encrypted []byte
|
||||
err := s.db.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT bit_length, data_encrypted FROM status_lists WHERE status_id = $1`,
|
||||
statusID,
|
||||
).Scan(&bitLength, &encrypted)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := s.desifriraj(encrypted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return statuslist.IzSurovihPodatkov(raw, bitLength)
|
||||
}
|
||||
|
||||
// Posodobi prepiše obstoječo status listo.
|
||||
func (s *PostgresStore) Posodobi(ctx context.Context, statusID string, list *statuslist.StatusList) error {
|
||||
encrypted, err := s.sifriraj(list.Bajti())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := s.db.ExecContext(
|
||||
ctx,
|
||||
`UPDATE status_lists SET bit_length = $2, data_encrypted = $3 WHERE status_id = $1`,
|
||||
statusID,
|
||||
list.DolzinaBitov(),
|
||||
encrypted,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sifriraj zasciti bajte z AES-GCM in pripne nonce.
|
||||
func (s *PostgresStore) sifriraj(plain []byte) ([]byte, error) {
|
||||
nonce := make([]byte, s.gc.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext := s.gc.Seal(nil, nonce, plain, nil)
|
||||
return append(nonce, ciphertext...), nil
|
||||
}
|
||||
|
||||
// desifriraj obnovi originalne bajte iz AES-GCM payloada.
|
||||
func (s *PostgresStore) desifriraj(encrypted []byte) ([]byte, error) {
|
||||
nonceSize := s.gc.NonceSize()
|
||||
if len(encrypted) < nonceSize {
|
||||
return nil, errors.New("encrypted payload too short")
|
||||
}
|
||||
nonce := encrypted[:nonceSize]
|
||||
ciphertext := encrypted[nonceSize:]
|
||||
return s.gc.Open(nil, nonce, ciphertext, nil)
|
||||
}
|
||||
14
internal/storage/store.go
Normal file
14
internal/storage/store.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"netisjwt/internal/statuslist"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
Ustvari(ctx context.Context, statusID string, list *statuslist.StatusList) error
|
||||
SeznamIDjev(ctx context.Context) ([]string, error)
|
||||
Dobi(ctx context.Context, statusID string) (*statuslist.StatusList, error)
|
||||
Posodobi(ctx context.Context, statusID string, list *statuslist.StatusList) error
|
||||
}
|
||||
Reference in New Issue
Block a user