feat(backend): implement db helpers and postgres pool/transactor
- Add is_blocked to core.users (002_core_tables.sql) - Add is_active to activity.sessions for soft deletes (005_activity_tables.sql) - Implement UserRepo: List, GetByID, GetByName, Create, Update, Delete - Implement MimeRepo: List, GetByID, GetByName - Implement SessionRepo: Create, GetByTokenHash, ListByUser, UpdateLastActivity, Delete, DeleteByUserID - Session deletes are soft (SET is_active = false); is_active is a SQL-only filter, not mapped to the domain type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2c83073903
commit
0e9b4637b0
97
backend/internal/db/postgres/mime_repo.go
Normal file
97
backend/internal/db/postgres/mime_repo.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"tanabata/backend/internal/domain"
|
||||||
|
"tanabata/backend/internal/port"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mimeRow struct {
|
||||||
|
ID int16 `db:"id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
Extension string `db:"extension"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toMIMEType(r mimeRow) domain.MIMEType {
|
||||||
|
return domain.MIMEType{
|
||||||
|
ID: r.ID,
|
||||||
|
Name: r.Name,
|
||||||
|
Extension: r.Extension,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MimeRepo implements port.MimeRepo using PostgreSQL.
|
||||||
|
type MimeRepo struct {
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMimeRepo creates a MimeRepo backed by pool.
|
||||||
|
func NewMimeRepo(pool *pgxpool.Pool) *MimeRepo {
|
||||||
|
return &MimeRepo{pool: pool}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ port.MimeRepo = (*MimeRepo)(nil)
|
||||||
|
|
||||||
|
func (r *MimeRepo) List(ctx context.Context) ([]domain.MIMEType, error) {
|
||||||
|
const sql = `SELECT id, name, extension FROM core.mime_types ORDER BY name`
|
||||||
|
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
rows, err := q.Query(ctx, sql)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("MimeRepo.List: %w", err)
|
||||||
|
}
|
||||||
|
collected, err := pgx.CollectRows(rows, pgx.RowToStructByName[mimeRow])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("MimeRepo.List scan: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]domain.MIMEType, len(collected))
|
||||||
|
for i, row := range collected {
|
||||||
|
result[i] = toMIMEType(row)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MimeRepo) GetByID(ctx context.Context, id int16) (*domain.MIMEType, error) {
|
||||||
|
const sql = `SELECT id, name, extension FROM core.mime_types WHERE id = $1`
|
||||||
|
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
rows, err := q.Query(ctx, sql, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("MimeRepo.GetByID: %w", err)
|
||||||
|
}
|
||||||
|
row, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[mimeRow])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("MimeRepo.GetByID scan: %w", err)
|
||||||
|
}
|
||||||
|
m := toMIMEType(row)
|
||||||
|
return &m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MimeRepo) GetByName(ctx context.Context, name string) (*domain.MIMEType, error) {
|
||||||
|
const sql = `SELECT id, name, extension FROM core.mime_types WHERE name = $1`
|
||||||
|
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
rows, err := q.Query(ctx, sql, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("MimeRepo.GetByName: %w", err)
|
||||||
|
}
|
||||||
|
row, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[mimeRow])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, domain.ErrUnsupportedMIME
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("MimeRepo.GetByName scan: %w", err)
|
||||||
|
}
|
||||||
|
m := toMIMEType(row)
|
||||||
|
return &m, nil
|
||||||
|
}
|
||||||
162
backend/internal/db/postgres/session_repo.go
Normal file
162
backend/internal/db/postgres/session_repo.go
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"tanabata/backend/internal/domain"
|
||||||
|
"tanabata/backend/internal/port"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sessionRow matches the columns stored in activity.sessions.
|
||||||
|
// IsCurrent is a service-layer concern and is not stored in the database.
|
||||||
|
type sessionRow struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
TokenHash string `db:"token_hash"`
|
||||||
|
UserID int16 `db:"user_id"`
|
||||||
|
UserAgent string `db:"user_agent"`
|
||||||
|
StartedAt time.Time `db:"started_at"`
|
||||||
|
ExpiresAt *time.Time `db:"expires_at"`
|
||||||
|
LastActivity time.Time `db:"last_activity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// sessionRowWithTotal extends sessionRow with a window-function count for ListByUser.
|
||||||
|
type sessionRowWithTotal struct {
|
||||||
|
sessionRow
|
||||||
|
Total int `db:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSession(r sessionRow) domain.Session {
|
||||||
|
return domain.Session{
|
||||||
|
ID: r.ID,
|
||||||
|
TokenHash: r.TokenHash,
|
||||||
|
UserID: r.UserID,
|
||||||
|
UserAgent: r.UserAgent,
|
||||||
|
StartedAt: r.StartedAt,
|
||||||
|
ExpiresAt: r.ExpiresAt,
|
||||||
|
LastActivity: r.LastActivity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionRepo implements port.SessionRepo using PostgreSQL.
|
||||||
|
type SessionRepo struct {
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSessionRepo creates a SessionRepo backed by pool.
|
||||||
|
func NewSessionRepo(pool *pgxpool.Pool) *SessionRepo {
|
||||||
|
return &SessionRepo{pool: pool}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ port.SessionRepo = (*SessionRepo)(nil)
|
||||||
|
|
||||||
|
func (r *SessionRepo) Create(ctx context.Context, s *domain.Session) (*domain.Session, error) {
|
||||||
|
const sql = `
|
||||||
|
INSERT INTO activity.sessions (token_hash, user_id, user_agent, expires_at)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, token_hash, user_id, user_agent, started_at, expires_at, last_activity`
|
||||||
|
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
rows, err := q.Query(ctx, sql, s.TokenHash, s.UserID, s.UserAgent, s.ExpiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("SessionRepo.Create: %w", err)
|
||||||
|
}
|
||||||
|
row, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[sessionRow])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("SessionRepo.Create scan: %w", err)
|
||||||
|
}
|
||||||
|
created := toSession(row)
|
||||||
|
return &created, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SessionRepo) GetByTokenHash(ctx context.Context, hash string) (*domain.Session, error) {
|
||||||
|
const sql = `
|
||||||
|
SELECT id, token_hash, user_id, user_agent, started_at, expires_at, last_activity
|
||||||
|
FROM activity.sessions
|
||||||
|
WHERE token_hash = $1 AND is_active = true`
|
||||||
|
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
rows, err := q.Query(ctx, sql, hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("SessionRepo.GetByTokenHash: %w", err)
|
||||||
|
}
|
||||||
|
row, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[sessionRow])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("SessionRepo.GetByTokenHash scan: %w", err)
|
||||||
|
}
|
||||||
|
s := toSession(row)
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SessionRepo) ListByUser(ctx context.Context, userID int16) (*domain.SessionList, error) {
|
||||||
|
const sql = `
|
||||||
|
SELECT id, token_hash, user_id, user_agent, started_at, expires_at, last_activity,
|
||||||
|
COUNT(*) OVER() AS total
|
||||||
|
FROM activity.sessions
|
||||||
|
WHERE user_id = $1 AND is_active = true
|
||||||
|
ORDER BY started_at DESC`
|
||||||
|
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
rows, err := q.Query(ctx, sql, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("SessionRepo.ListByUser: %w", err)
|
||||||
|
}
|
||||||
|
collected, err := pgx.CollectRows(rows, pgx.RowToStructByName[sessionRowWithTotal])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("SessionRepo.ListByUser scan: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
list := &domain.SessionList{}
|
||||||
|
if len(collected) > 0 {
|
||||||
|
list.Total = collected[0].Total
|
||||||
|
}
|
||||||
|
list.Items = make([]domain.Session, len(collected))
|
||||||
|
for i, row := range collected {
|
||||||
|
list.Items[i] = toSession(row.sessionRow)
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SessionRepo) UpdateLastActivity(ctx context.Context, id int, t time.Time) error {
|
||||||
|
const sql = `UPDATE activity.sessions SET last_activity = $2 WHERE id = $1`
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
tag, err := q.Exec(ctx, sql, id, t)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SessionRepo.UpdateLastActivity: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return domain.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SessionRepo) Delete(ctx context.Context, id int) error {
|
||||||
|
const sql = `UPDATE activity.sessions SET is_active = false WHERE id = $1 AND is_active = true`
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
tag, err := q.Exec(ctx, sql, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SessionRepo.Delete: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return domain.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SessionRepo) DeleteByUserID(ctx context.Context, userID int16) error {
|
||||||
|
const sql = `UPDATE activity.sessions SET is_active = false WHERE user_id = $1 AND is_active = true`
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
_, err := q.Exec(ctx, sql, userID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SessionRepo.DeleteByUserID: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
196
backend/internal/db/postgres/user_repo.go
Normal file
196
backend/internal/db/postgres/user_repo.go
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"tanabata/backend/internal/db"
|
||||||
|
"tanabata/backend/internal/domain"
|
||||||
|
"tanabata/backend/internal/port"
|
||||||
|
)
|
||||||
|
|
||||||
|
// userRow matches the columns returned by every user SELECT.
|
||||||
|
type userRow struct {
|
||||||
|
ID int16 `db:"id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
Password string `db:"password"`
|
||||||
|
IsAdmin bool `db:"is_admin"`
|
||||||
|
CanCreate bool `db:"can_create"`
|
||||||
|
IsBlocked bool `db:"is_blocked"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// userRowWithTotal extends userRow with a window-function total for List.
|
||||||
|
type userRowWithTotal struct {
|
||||||
|
userRow
|
||||||
|
Total int `db:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUser(r userRow) domain.User {
|
||||||
|
return domain.User{
|
||||||
|
ID: r.ID,
|
||||||
|
Name: r.Name,
|
||||||
|
Password: r.Password,
|
||||||
|
IsAdmin: r.IsAdmin,
|
||||||
|
CanCreate: r.CanCreate,
|
||||||
|
IsBlocked: r.IsBlocked,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// userSortColumn whitelists valid sort keys to prevent SQL injection.
|
||||||
|
var userSortColumn = map[string]string{
|
||||||
|
"name": "name",
|
||||||
|
"id": "id",
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserRepo implements port.UserRepo using PostgreSQL.
|
||||||
|
type UserRepo struct {
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserRepo creates a UserRepo backed by pool.
|
||||||
|
func NewUserRepo(pool *pgxpool.Pool) *UserRepo {
|
||||||
|
return &UserRepo{pool: pool}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ port.UserRepo = (*UserRepo)(nil)
|
||||||
|
|
||||||
|
func (r *UserRepo) List(ctx context.Context, params port.OffsetParams) (*domain.UserPage, error) {
|
||||||
|
col, ok := userSortColumn[params.Sort]
|
||||||
|
if !ok {
|
||||||
|
col = "id"
|
||||||
|
}
|
||||||
|
ord := "ASC"
|
||||||
|
if params.Order == "desc" {
|
||||||
|
ord = "DESC"
|
||||||
|
}
|
||||||
|
limit := db.ClampLimit(params.Limit, 50, 200)
|
||||||
|
offset := db.ClampOffset(params.Offset)
|
||||||
|
|
||||||
|
sql := fmt.Sprintf(`
|
||||||
|
SELECT id, name, password, is_admin, can_create, is_blocked,
|
||||||
|
COUNT(*) OVER() AS total
|
||||||
|
FROM core.users
|
||||||
|
ORDER BY %s %s
|
||||||
|
LIMIT $1 OFFSET $2`, col, ord)
|
||||||
|
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
rows, err := q.Query(ctx, sql, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("UserRepo.List: %w", err)
|
||||||
|
}
|
||||||
|
collected, err := pgx.CollectRows(rows, pgx.RowToStructByName[userRowWithTotal])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("UserRepo.List scan: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
page := &domain.UserPage{Offset: offset, Limit: limit}
|
||||||
|
if len(collected) > 0 {
|
||||||
|
page.Total = collected[0].Total
|
||||||
|
}
|
||||||
|
page.Items = make([]domain.User, len(collected))
|
||||||
|
for i, row := range collected {
|
||||||
|
page.Items[i] = toUser(row.userRow)
|
||||||
|
}
|
||||||
|
return page, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepo) GetByID(ctx context.Context, id int16) (*domain.User, error) {
|
||||||
|
const sql = `
|
||||||
|
SELECT id, name, password, is_admin, can_create, is_blocked
|
||||||
|
FROM core.users WHERE id = $1`
|
||||||
|
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
rows, err := q.Query(ctx, sql, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("UserRepo.GetByID: %w", err)
|
||||||
|
}
|
||||||
|
row, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[userRow])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("UserRepo.GetByID scan: %w", err)
|
||||||
|
}
|
||||||
|
u := toUser(row)
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepo) GetByName(ctx context.Context, name string) (*domain.User, error) {
|
||||||
|
const sql = `
|
||||||
|
SELECT id, name, password, is_admin, can_create, is_blocked
|
||||||
|
FROM core.users WHERE name = $1`
|
||||||
|
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
rows, err := q.Query(ctx, sql, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("UserRepo.GetByName: %w", err)
|
||||||
|
}
|
||||||
|
row, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[userRow])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("UserRepo.GetByName scan: %w", err)
|
||||||
|
}
|
||||||
|
u := toUser(row)
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepo) Create(ctx context.Context, u *domain.User) (*domain.User, error) {
|
||||||
|
const sql = `
|
||||||
|
INSERT INTO core.users (name, password, is_admin, can_create)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, name, password, is_admin, can_create, is_blocked`
|
||||||
|
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
rows, err := q.Query(ctx, sql, u.Name, u.Password, u.IsAdmin, u.CanCreate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("UserRepo.Create: %w", err)
|
||||||
|
}
|
||||||
|
row, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[userRow])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("UserRepo.Create scan: %w", err)
|
||||||
|
}
|
||||||
|
created := toUser(row)
|
||||||
|
return &created, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepo) Update(ctx context.Context, id int16, u *domain.User) (*domain.User, error) {
|
||||||
|
const sql = `
|
||||||
|
UPDATE core.users
|
||||||
|
SET name = $2, password = $3, is_admin = $4, can_create = $5
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, name, password, is_admin, can_create, is_blocked`
|
||||||
|
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
rows, err := q.Query(ctx, sql, id, u.Name, u.Password, u.IsAdmin, u.CanCreate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("UserRepo.Update: %w", err)
|
||||||
|
}
|
||||||
|
row, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[userRow])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("UserRepo.Update scan: %w", err)
|
||||||
|
}
|
||||||
|
updated := toUser(row)
|
||||||
|
return &updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepo) Delete(ctx context.Context, id int16) error {
|
||||||
|
const sql = `DELETE FROM core.users WHERE id = $1`
|
||||||
|
q := connOrTx(ctx, r.pool)
|
||||||
|
tag, err := q.Exec(ctx, sql, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("UserRepo.Delete: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return domain.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ CREATE TABLE core.users (
|
|||||||
password text NOT NULL, -- bcrypt hash via pgcrypto
|
password text NOT NULL, -- bcrypt hash via pgcrypto
|
||||||
is_admin boolean NOT NULL DEFAULT false,
|
is_admin boolean NOT NULL DEFAULT false,
|
||||||
can_create boolean NOT NULL DEFAULT false,
|
can_create boolean NOT NULL DEFAULT false,
|
||||||
|
is_blocked boolean NOT NULL DEFAULT false,
|
||||||
|
|
||||||
CONSTRAINT uni__users__name UNIQUE (name)
|
CONSTRAINT uni__users__name UNIQUE (name)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -16,6 +16,7 @@ CREATE TABLE activity.sessions (
|
|||||||
started_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
started_at timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||||
expires_at timestamptz,
|
expires_at timestamptz,
|
||||||
last_activity timestamptz NOT NULL DEFAULT statement_timestamp(),
|
last_activity timestamptz NOT NULL DEFAULT statement_timestamp(),
|
||||||
|
is_active boolean NOT NULL DEFAULT true,
|
||||||
|
|
||||||
CONSTRAINT uni__sessions__token_hash UNIQUE (token_hash)
|
CONSTRAINT uni__sessions__token_hash UNIQUE (token_hash)
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user