extend database
This commit is contained in:
parent
8e986c15b2
commit
6d25a0928f
25 changed files with 896 additions and 502 deletions
6
Makefile
6
Makefile
|
@ -18,9 +18,9 @@ dev:
|
|||
migrate: .env
|
||||
$(GORUN) --tags=dev main.go migrate -u
|
||||
|
||||
.PHONY: migratedown
|
||||
migratedown: .env
|
||||
$(GORUN) --tags=dev main.go migrate --revision=0
|
||||
.PHONY: drop
|
||||
drop: .env
|
||||
$(GORUN) --tags=dev main.go migrate --drop-all
|
||||
|
||||
.PHONY: generate
|
||||
generate:
|
||||
|
|
|
@ -1 +1,6 @@
|
|||
-- this file is only here so the database can track a completely empty state.
|
||||
|
||||
-- pgcrypto adds functions for generating UUIDs.
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
-- citext adds indexable case-insensitive text fields.
|
||||
CREATE EXTENSION IF NOT EXISTS "citext";
|
|
@ -1,4 +1,5 @@
|
|||
DROP TABLE "reset";
|
||||
DROP TABLE "confirmation";
|
||||
DROP TABLE "email";
|
||||
DROP TABLE "user";
|
||||
|
||||
DROP TABLE "person";
|
||||
|
||||
DROP TABLE "identity";
|
||||
|
|
|
@ -1,37 +1,51 @@
|
|||
CREATE TABLE "user" (
|
||||
"id" bigserial NOT NULL,
|
||||
"is_admin" boolean NOT NULL DEFAULT false,
|
||||
"password" bytea NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT NOW(),
|
||||
-- An Identity is any object that can participate as an actor in the system.
|
||||
-- It can have groups, permissions, own other objects etc.
|
||||
CREATE TABLE "identity" (
|
||||
"id" bigserial NOT NULL,
|
||||
"login" citext NULL,
|
||||
"passphrase" bytea NULL,
|
||||
"totp_secret" text NULL,
|
||||
"is_admin" boolean NOT NULL DEFAULT false,
|
||||
"is_disabled" boolean NOT NULL DEFAULT false,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE UNIQUE INDEX "identity_login_key" ON "identity" ("login");
|
||||
|
||||
-- A person is a human actor within the system, it is linked to exactly one
|
||||
-- identity.
|
||||
CREATE TABLE "person" (
|
||||
"identity_id" bigint NOT NULL,
|
||||
"display_name" text NULL,
|
||||
"first_name" text NULL,
|
||||
"last_name" text NULL,
|
||||
"image_url" text NULL,
|
||||
"zoneinfo" text NULL,
|
||||
"locale" text NULL,
|
||||
FOREIGN KEY ("identity_id")
|
||||
REFERENCES "identity" ("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE RESTRICT,
|
||||
PRIMARY KEY ("identity_id")
|
||||
);
|
||||
|
||||
-- Email is an email address for an identity (most likely for a person),
|
||||
-- that may be verified. Zero or one email address assigned to the identity
|
||||
-- may be "primary", e.g. used for notifications or login.
|
||||
CREATE TABLE "email" (
|
||||
"address" text NOT NULL,
|
||||
"user_id" bigint NOT NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT NOW(),
|
||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
"address" citext NOT NULL,
|
||||
"identity_id" bigint NOT NULL,
|
||||
"is_verified" boolean NOT NULL DEFAULT false,
|
||||
"is_primary" boolean NOT NULL DEFAULT false,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
FOREIGN KEY ("identity_id")
|
||||
REFERENCES "identity" ("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE RESTRICT,
|
||||
PRIMARY KEY ("address")
|
||||
);
|
||||
CREATE INDEX ON "email" ("user_id");
|
||||
|
||||
CREATE TABLE "confirmation" (
|
||||
"email_address" text NOT NULL,
|
||||
"user_id" bigint NOT NULL,
|
||||
"selector" text NOT NULL,
|
||||
"verifier" bytea NOT NULL, -- hashed
|
||||
"expires_at" timestamptz NOT NULL DEFAULT NOW(),
|
||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
PRIMARY KEY ("selector")
|
||||
);
|
||||
CREATE INDEX ON "confirmation" ("user_id");
|
||||
|
||||
CREATE TABLE "reset" (
|
||||
"user_id" bigint NOT NULL,
|
||||
"selector" text NOT NULL,
|
||||
"verifier" bytea NOT NULL, -- hashed
|
||||
"expires_at" timestamptz NOT NULL DEFAULT NOW(),
|
||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
PRIMARY KEY ("selector")
|
||||
);
|
||||
CREATE UNIQUE INDEX ON "reset" ("user_id");
|
||||
CREATE INDEX "email_is_verified_idx" ON "email" ("is_verified")
|
||||
WHERE "is_verified" = true;
|
||||
CREATE INDEX "email_identity_id_idx" ON "email" ("identity_id");
|
||||
CREATE UNIQUE INDEX "email_is_primary_key" ON "email" ("identity_id", "is_primary")
|
||||
WHERE "is_primary" = true;
|
3
assets/migrations/2_email_tokens.down.sql
Normal file
3
assets/migrations/2_email_tokens.down.sql
Normal file
|
@ -0,0 +1,3 @@
|
|||
DROP TABLE "email_confirmation";
|
||||
|
||||
DROP TABLE "password_reset";
|
28
assets/migrations/2_email_tokens.up.sql
Normal file
28
assets/migrations/2_email_tokens.up.sql
Normal file
|
@ -0,0 +1,28 @@
|
|||
-- Email Confirmation tracks all email confirmations that have been sent out.
|
||||
CREATE TABLE "email_confirmation" (
|
||||
"email_address" citext NOT NULL,
|
||||
"selector" text NOT NULL,
|
||||
"verifier" bytea NOT NULL,
|
||||
"valid_until" timestamptz NOT NULL,
|
||||
FOREIGN KEY ("email_address")
|
||||
REFERENCES "email" ("address")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE RESTRICT,
|
||||
PRIMARY KEY ("email_address")
|
||||
);
|
||||
CREATE UNIQUE INDEX "email_confirmation_selector_key"
|
||||
ON "email_confirmation" ("selector");
|
||||
|
||||
-- Password reset keeps track of the password reset tokens.
|
||||
CREATE TABLE "password_reset" (
|
||||
"identity_id" bigserial NOT NULL,
|
||||
"selector" text NOT NULL,
|
||||
"verifier" bytea NOT NULL,
|
||||
"valid_until" timestamptz NOT NULL,
|
||||
FOREIGN KEY ("identity_id")
|
||||
REFERENCES "person" ("identity_id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE RESTRICT,
|
||||
PRIMARY KEY ("identity_id")
|
||||
);
|
||||
CREATE UNIQUE INDEX "password_reset_selector_key" ON "password_reset" ("selector");
|
|
@ -1,17 +0,0 @@
|
|||
CREATE TABLE "external_auth" (
|
||||
"id" bigserial NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"config" jsonb NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE INDEX ON "external_auth" ("type");
|
||||
|
||||
CREATE TABLE "external_user" (
|
||||
"external_auth_id" bigint NOT NULL,
|
||||
"foreign_id" text NOT NULL,
|
||||
"user_id" bigint NOT NULL,
|
||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("external_auth_id") REFERENCES "external_auth" ("id") ON DELETE CASCADE,
|
||||
PRIMARY KEY ("external_auth_id", "foreign_id")
|
||||
);
|
33
assets/migrations/3_external.up.sql
Normal file
33
assets/migrations/3_external.up.sql
Normal file
|
@ -0,0 +1,33 @@
|
|||
CREATE TABLE "external_auth" (
|
||||
"name" text NOT NULL,
|
||||
"oidc_url" text NULL,
|
||||
"auth_url" text NOT NULL,
|
||||
"token_url" text NOT NULL,
|
||||
"client_key" text NOT NULL,
|
||||
"client_secret" text NOT NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY ("name")
|
||||
);
|
||||
CREATE UNIQUE INDEX "external_auth_name_key" ON "external_auth" ("name");
|
||||
|
||||
CREATE TABLE "external_user" (
|
||||
"identity_id" bigint NOT NULL,
|
||||
"external_auth_name" text NOT NULL,
|
||||
"external_id" text NOT NULL,
|
||||
"auth_token" text NULL,
|
||||
"refresh_token" text NULL,
|
||||
"identity_token" text NULL,
|
||||
FOREIGN KEY ("identity_id")
|
||||
REFERENCES "identity" ("id")
|
||||
ON UPDATE RESTRICT
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY ("external_auth_name")
|
||||
REFERENCES "external_auth" ("name")
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
PRIMARY KEY ("identity_id")
|
||||
);
|
||||
CREATE INDEX "external_user_external_auth_name_idx"
|
||||
ON "external_user" ("external_auth_name");
|
||||
CREATE UNIQUE INDEX "external_user_external_id_key"
|
||||
ON "external_user" ("external_auth_name", "external_id");
|
|
@ -8,86 +8,89 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const createConfirmation = `-- name: CreateConfirmation :one
|
||||
INSERT INTO "public"."confirmation" (
|
||||
"email_address", "user_id", "selector", "verifier", "expires_at"
|
||||
const createEmailConfirmation = `-- name: CreateEmailConfirmation :one
|
||||
INSERT INTO "email_confirmation" (
|
||||
"selector",
|
||||
"verifier",
|
||||
"valid_until",
|
||||
"email_address"
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5
|
||||
) RETURNING email_address, user_id, selector, verifier, expires_at
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4
|
||||
) RETURNING email_address, selector, verifier, valid_until
|
||||
`
|
||||
|
||||
type CreateConfirmationParams struct {
|
||||
EmailAddress string `json:"email_address"`
|
||||
UserID int64 `json:"user_id"`
|
||||
type CreateEmailConfirmationParams struct {
|
||||
Selector string `json:"selector"`
|
||||
Verifier []byte `json:"verifier"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
EmailAddress string `json:"email_address"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateConfirmation(ctx context.Context, arg CreateConfirmationParams) (Confirmation, error) {
|
||||
row := q.db.QueryRowContext(ctx, createConfirmation,
|
||||
arg.EmailAddress,
|
||||
arg.UserID,
|
||||
func (q *Queries) CreateEmailConfirmation(ctx context.Context, arg CreateEmailConfirmationParams) (EmailConfirmation, error) {
|
||||
row := q.db.QueryRowContext(ctx, createEmailConfirmation,
|
||||
arg.Selector,
|
||||
arg.Verifier,
|
||||
arg.ExpiresAt,
|
||||
arg.ValidUntil,
|
||||
arg.EmailAddress,
|
||||
)
|
||||
var i Confirmation
|
||||
var i EmailConfirmation
|
||||
err := row.Scan(
|
||||
&i.EmailAddress,
|
||||
&i.UserID,
|
||||
&i.Selector,
|
||||
&i.Verifier,
|
||||
&i.ExpiresAt,
|
||||
&i.ValidUntil,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const destroyConfirmation = `-- name: DestroyConfirmation :exec
|
||||
DELETE FROM "public"."confirmation" WHERE "selector" = $1
|
||||
const destroyEmailConfirmation = `-- name: DestroyEmailConfirmation :exec
|
||||
DELETE FROM "email_confirmation" WHERE "email_address" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DestroyConfirmation(ctx context.Context, selector string) error {
|
||||
_, err := q.db.ExecContext(ctx, destroyConfirmation, selector)
|
||||
func (q *Queries) DestroyEmailConfirmation(ctx context.Context, emailAddress string) error {
|
||||
_, err := q.db.ExecContext(ctx, destroyEmailConfirmation, emailAddress)
|
||||
return err
|
||||
}
|
||||
|
||||
const getConfirmationBySelector = `-- name: GetConfirmationBySelector :one
|
||||
const getEmailConfirmationByAddress = `-- name: GetEmailConfirmationByAddress :one
|
||||
SELECT
|
||||
"email_address", "user_id", "selector", "verifier", "expires_at"
|
||||
FROM "public"."confirmation"
|
||||
email_address, selector, verifier, valid_until
|
||||
FROM "email_confirmation"
|
||||
WHERE "email_address" = $1
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetEmailConfirmationByAddress(ctx context.Context, emailAddress string) (EmailConfirmation, error) {
|
||||
row := q.db.QueryRowContext(ctx, getEmailConfirmationByAddress, emailAddress)
|
||||
var i EmailConfirmation
|
||||
err := row.Scan(
|
||||
&i.EmailAddress,
|
||||
&i.Selector,
|
||||
&i.Verifier,
|
||||
&i.ValidUntil,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getEmailConfirmationBySelector = `-- name: GetEmailConfirmationBySelector :one
|
||||
SELECT
|
||||
email_address, selector, verifier, valid_until
|
||||
FROM "email_confirmation"
|
||||
WHERE "selector" = $1
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetConfirmationBySelector(ctx context.Context, selector string) (Confirmation, error) {
|
||||
row := q.db.QueryRowContext(ctx, getConfirmationBySelector, selector)
|
||||
var i Confirmation
|
||||
func (q *Queries) GetEmailConfirmationBySelector(ctx context.Context, selector string) (EmailConfirmation, error) {
|
||||
row := q.db.QueryRowContext(ctx, getEmailConfirmationBySelector, selector)
|
||||
var i EmailConfirmation
|
||||
err := row.Scan(
|
||||
&i.EmailAddress,
|
||||
&i.UserID,
|
||||
&i.Selector,
|
||||
&i.Verifier,
|
||||
&i.ExpiresAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getConfirmationByUserID = `-- name: GetConfirmationByUserID :one
|
||||
SELECT
|
||||
"email_address", "user_id", "selector", "verifier", "expires_at"
|
||||
FROM "public"."confirmation"
|
||||
WHERE "user_id" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetConfirmationByUserID(ctx context.Context, userID int64) (Confirmation, error) {
|
||||
row := q.db.QueryRowContext(ctx, getConfirmationByUserID, userID)
|
||||
var i Confirmation
|
||||
err := row.Scan(
|
||||
&i.EmailAddress,
|
||||
&i.UserID,
|
||||
&i.Selector,
|
||||
&i.Verifier,
|
||||
&i.ExpiresAt,
|
||||
&i.ValidUntil,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
@ -5,27 +5,67 @@ package database
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
const countEmailsByIdentityID = `-- name: CountEmailsByIdentityID :one
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
"email"
|
||||
WHERE
|
||||
"identity_id" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) CountEmailsByIdentityID(ctx context.Context, identityID int64) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, countEmailsByIdentityID, identityID)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const countUnverifiedEmailsByIdentityID = `-- name: CountUnverifiedEmailsByIdentityID :one
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
"email"
|
||||
WHERE
|
||||
"identity_id" = $1 AND
|
||||
"is_verified" = FALSE
|
||||
`
|
||||
|
||||
func (q *Queries) CountUnverifiedEmailsByIdentityID(ctx context.Context, identityID int64) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, countUnverifiedEmailsByIdentityID, identityID)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const createEmail = `-- name: CreateEmail :one
|
||||
INSERT INTO "email" (
|
||||
"address", "user_id", "created_at"
|
||||
"address",
|
||||
"identity_id",
|
||||
"is_verified"
|
||||
) VALUES (
|
||||
$1, $2, $3
|
||||
) RETURNING address, user_id, created_at
|
||||
$1, $2, $3
|
||||
) RETURNING address, identity_id, is_verified, is_primary, created_at
|
||||
`
|
||||
|
||||
type CreateEmailParams struct {
|
||||
Address string `json:"address"`
|
||||
UserID int64 `json:"user_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Address string `json:"address"`
|
||||
IdentityID int64 `json:"identity_id"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateEmail(ctx context.Context, arg CreateEmailParams) (Email, error) {
|
||||
row := q.db.QueryRowContext(ctx, createEmail, arg.Address, arg.UserID, arg.CreatedAt)
|
||||
row := q.db.QueryRowContext(ctx, createEmail, arg.Address, arg.IdentityID, arg.IsVerified)
|
||||
var i Email
|
||||
err := row.Scan(&i.Address, &i.UserID, &i.CreatedAt)
|
||||
err := row.Scan(
|
||||
&i.Address,
|
||||
&i.IdentityID,
|
||||
&i.IsVerified,
|
||||
&i.IsPrimary,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
|
@ -40,47 +80,118 @@ func (q *Queries) DestroyEmail(ctx context.Context, address string) error {
|
|||
|
||||
const getEmailByAddress = `-- name: GetEmailByAddress :one
|
||||
SELECT
|
||||
"address", "user_id", "created_at"
|
||||
FROM "public"."email"
|
||||
WHERE "address" = $1
|
||||
address, identity_id, is_verified, is_primary, created_at
|
||||
FROM "email"
|
||||
WHERE
|
||||
"address" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetEmailByAddress(ctx context.Context, address string) (Email, error) {
|
||||
row := q.db.QueryRowContext(ctx, getEmailByAddress, address)
|
||||
var i Email
|
||||
err := row.Scan(&i.Address, &i.UserID, &i.CreatedAt)
|
||||
err := row.Scan(
|
||||
&i.Address,
|
||||
&i.IdentityID,
|
||||
&i.IsVerified,
|
||||
&i.IsPrimary,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getEmailByUserID = `-- name: GetEmailByUserID :one
|
||||
const getEmailByIdentityID = `-- name: GetEmailByIdentityID :many
|
||||
SELECT
|
||||
"address", "user_id", "created_at"
|
||||
FROM "public"."email"
|
||||
WHERE "user_id" = $1
|
||||
address, identity_id, is_verified, is_primary, created_at
|
||||
FROM "email"
|
||||
WHERE
|
||||
"identity_id" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetEmailByUserID(ctx context.Context, userID int64) (Email, error) {
|
||||
row := q.db.QueryRowContext(ctx, getEmailByUserID, userID)
|
||||
func (q *Queries) GetEmailByIdentityID(ctx context.Context, identityID int64) ([]Email, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getEmailByIdentityID, identityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Email
|
||||
for rows.Next() {
|
||||
var i Email
|
||||
if err := rows.Scan(
|
||||
&i.Address,
|
||||
&i.IdentityID,
|
||||
&i.IsVerified,
|
||||
&i.IsPrimary,
|
||||
&i.CreatedAt,
|
||||
); 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 getPrimaryEmailByIdentityID = `-- name: GetPrimaryEmailByIdentityID :one
|
||||
SELECT
|
||||
address, identity_id, is_verified, is_primary, created_at
|
||||
FROM "email"
|
||||
WHERE
|
||||
"identity_id" = $1 AND "is_primary"
|
||||
`
|
||||
|
||||
func (q *Queries) GetPrimaryEmailByIdentityID(ctx context.Context, identityID int64) (Email, error) {
|
||||
row := q.db.QueryRowContext(ctx, getPrimaryEmailByIdentityID, identityID)
|
||||
var i Email
|
||||
err := row.Scan(&i.Address, &i.UserID, &i.CreatedAt)
|
||||
err := row.Scan(
|
||||
&i.Address,
|
||||
&i.IdentityID,
|
||||
&i.IsVerified,
|
||||
&i.IsPrimary,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateEmail = `-- name: UpdateEmail :exec
|
||||
UPDATE "email" SET (
|
||||
"user_id", "created_at"
|
||||
) = (
|
||||
$1, $2
|
||||
) WHERE "address" = $3
|
||||
const updateEmailPrimary = `-- name: UpdateEmailPrimary :exec
|
||||
UPDATE "email"
|
||||
SET
|
||||
"is_primary" = NOT "is_primary"
|
||||
WHERE
|
||||
"identity_id" = $1 AND (
|
||||
( "address" <> $2 AND "is_primary" = true ) OR
|
||||
( "address" = $2 AND "is_primary" = false AND "is_verified" = true )
|
||||
)
|
||||
`
|
||||
|
||||
type UpdateEmailParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Address string `json:"address"`
|
||||
type UpdateEmailPrimaryParams struct {
|
||||
IdentityID int64 `json:"identity_id"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateEmail(ctx context.Context, arg UpdateEmailParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateEmail, arg.UserID, arg.CreatedAt, arg.Address)
|
||||
// UpdateEmailPrimary sets exactly one primary email for an identity.
|
||||
// The mail address has to have been verified.
|
||||
// this query basically combines these two queries into one atomic one:
|
||||
// UPDATE "email" SET "is_primary" = false WHERE "identity_id" = $1;
|
||||
// UPDATE "email" SET "is_primary" = true WHERE "identity_id" = $1 AND "address" = $2 AND "is_verified" = true;
|
||||
func (q *Queries) UpdateEmailPrimary(ctx context.Context, arg UpdateEmailPrimaryParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateEmailPrimary, arg.IdentityID, arg.Address)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateEmailVerified = `-- name: UpdateEmailVerified :exec
|
||||
UPDATE "email" SET (
|
||||
"is_verified"
|
||||
) = (
|
||||
$2
|
||||
) WHERE "address" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) UpdateEmailVerified(ctx context.Context, address string) error {
|
||||
_, err := q.db.ExecContext(ctx, updateEmailVerified, address)
|
||||
return err
|
||||
}
|
||||
|
|
164
internal/database/identity.sql.go
Normal file
164
internal/database/identity.sql.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// source: identity.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
const createIdentity = `-- name: CreateIdentity :one
|
||||
INSERT INTO "identity" (
|
||||
"login",
|
||||
"passphrase",
|
||||
"is_admin",
|
||||
"is_disabled"
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4
|
||||
) RETURNING id, login, passphrase, totp_secret, is_admin, is_disabled, created_at
|
||||
`
|
||||
|
||||
type CreateIdentityParams struct {
|
||||
Login sql.NullString `json:"login"`
|
||||
Passphrase []byte `json:"passphrase"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
IsDisabled bool `json:"is_disabled"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateIdentity(ctx context.Context, arg CreateIdentityParams) (Identity, error) {
|
||||
row := q.db.QueryRowContext(ctx, createIdentity,
|
||||
arg.Login,
|
||||
arg.Passphrase,
|
||||
arg.IsAdmin,
|
||||
arg.IsDisabled,
|
||||
)
|
||||
var i Identity
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Login,
|
||||
&i.Passphrase,
|
||||
&i.TotpSecret,
|
||||
&i.IsAdmin,
|
||||
&i.IsDisabled,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getIdentityByID = `-- name: GetIdentityByID :one
|
||||
SELECT id, login, passphrase, totp_secret, is_admin, is_disabled, created_at FROM "identity"
|
||||
WHERE "id" = $1
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetIdentityByID(ctx context.Context, id int64) (Identity, error) {
|
||||
row := q.db.QueryRowContext(ctx, getIdentityByID, id)
|
||||
var i Identity
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Login,
|
||||
&i.Passphrase,
|
||||
&i.TotpSecret,
|
||||
&i.IsAdmin,
|
||||
&i.IsDisabled,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getIdentityByLogin = `-- name: GetIdentityByLogin :one
|
||||
SELECT id, login, passphrase, totp_secret, is_admin, is_disabled, created_at FROM "identity"
|
||||
WHERE "login" = $1
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetIdentityByLogin(ctx context.Context, login sql.NullString) (Identity, error) {
|
||||
row := q.db.QueryRowContext(ctx, getIdentityByLogin, login)
|
||||
var i Identity
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Login,
|
||||
&i.Passphrase,
|
||||
&i.TotpSecret,
|
||||
&i.IsAdmin,
|
||||
&i.IsDisabled,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getIdentityByPrimaryEmail = `-- name: GetIdentityByPrimaryEmail :one
|
||||
SELECT id, login, passphrase, totp_secret, is_admin, is_disabled, identity.created_at, address, identity_id, is_verified, is_primary, email.created_at FROM "identity"
|
||||
INNER JOIN "email"
|
||||
ON "email"."identity_id" = "identity"."id"
|
||||
WHERE
|
||||
"email"."address" = $1 AND
|
||||
"email"."is_primary"
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetIdentityByPrimaryEmailRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Login sql.NullString `json:"login"`
|
||||
Passphrase []byte `json:"passphrase"`
|
||||
TotpSecret sql.NullString `json:"totp_secret"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
IsDisabled bool `json:"is_disabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Address string `json:"address"`
|
||||
IdentityID int64 `json:"identity_id"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
CreatedAt_2 time.Time `json:"created_at_2"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetIdentityByPrimaryEmail(ctx context.Context, address string) (GetIdentityByPrimaryEmailRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getIdentityByPrimaryEmail, address)
|
||||
var i GetIdentityByPrimaryEmailRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Login,
|
||||
&i.Passphrase,
|
||||
&i.TotpSecret,
|
||||
&i.IsAdmin,
|
||||
&i.IsDisabled,
|
||||
&i.CreatedAt,
|
||||
&i.Address,
|
||||
&i.IdentityID,
|
||||
&i.IsVerified,
|
||||
&i.IsPrimary,
|
||||
&i.CreatedAt_2,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateIdentityLogin = `-- name: UpdateIdentityLogin :exec
|
||||
UPDATE "identity" SET (
|
||||
"login"
|
||||
) = (
|
||||
$2
|
||||
) WHERE "id" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) UpdateIdentityLogin(ctx context.Context, id int64) error {
|
||||
_, err := q.db.ExecContext(ctx, updateIdentityLogin, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateIdentityPassphrase = `-- name: UpdateIdentityPassphrase :exec
|
||||
UPDATE "identity" SET (
|
||||
"passphrase"
|
||||
) = (
|
||||
$2
|
||||
) WHERE "id" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) UpdateIdentityPassphrase(ctx context.Context, id int64) error {
|
||||
_, err := q.db.ExecContext(ctx, updateIdentityPassphrase, id)
|
||||
return err
|
||||
}
|
|
@ -54,6 +54,17 @@ const (
|
|||
MigrateUp = -1
|
||||
)
|
||||
|
||||
// GetCurrentMigration returns the currently active migration version.
|
||||
// If no migration has been applied yet, it will return ErrNilVersion.
|
||||
func GetCurrentMigration(db *sqlx.DB) (version uint, dirty bool, err error) {
|
||||
migrator, err := getMigrator(db.DB, http.Dir("/invalid/path/unused"), "/unused")
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
return migrator.Version()
|
||||
}
|
||||
|
||||
// Migrate Migrates the schema of the supplied Database. Supported
|
||||
// methods are:
|
||||
// * database.MigrateUp – Migrate to the latest version
|
||||
|
|
|
@ -3,47 +3,67 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Confirmation struct {
|
||||
EmailAddress string `json:"email_address"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Selector string `json:"selector"`
|
||||
Verifier []byte `json:"verifier"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
type Email struct {
|
||||
Address string `json:"address"`
|
||||
IdentityID int64 `json:"identity_id"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type Email struct {
|
||||
Address string `json:"address"`
|
||||
UserID int64 `json:"user_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
type EmailConfirmation struct {
|
||||
EmailAddress string `json:"email_address"`
|
||||
Selector string `json:"selector"`
|
||||
Verifier []byte `json:"verifier"`
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
}
|
||||
|
||||
type ExternalAuth struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Config json.RawMessage `json:"config"`
|
||||
Name string `json:"name"`
|
||||
OidcUrl sql.NullString `json:"oidc_url"`
|
||||
AuthUrl string `json:"auth_url"`
|
||||
TokenUrl string `json:"token_url"`
|
||||
ClientKey string `json:"client_key"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type ExternalUser struct {
|
||||
ExternalAuthID int64 `json:"external_auth_id"`
|
||||
ForeignID string `json:"foreign_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
IdentityID int64 `json:"identity_id"`
|
||||
ExternalAuthName string `json:"external_auth_name"`
|
||||
ExternalID string `json:"external_id"`
|
||||
AuthToken sql.NullString `json:"auth_token"`
|
||||
RefreshToken sql.NullString `json:"refresh_token"`
|
||||
IdentityToken sql.NullString `json:"identity_token"`
|
||||
}
|
||||
|
||||
type Reset struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Selector string `json:"selector"`
|
||||
Verifier []byte `json:"verifier"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
type Identity struct {
|
||||
ID int64 `json:"id"`
|
||||
Login sql.NullString `json:"login"`
|
||||
Passphrase []byte `json:"passphrase"`
|
||||
TotpSecret sql.NullString `json:"totp_secret"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
IsDisabled bool `json:"is_disabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Password []byte `json:"password"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
type PasswordReset struct {
|
||||
IdentityID int64 `json:"identity_id"`
|
||||
Selector string `json:"selector"`
|
||||
Verifier []byte `json:"verifier"`
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
IdentityID int64 `json:"identity_id"`
|
||||
DisplayName sql.NullString `json:"display_name"`
|
||||
FirstName sql.NullString `json:"first_name"`
|
||||
LastName sql.NullString `json:"last_name"`
|
||||
ImageUrl sql.NullString `json:"image_url"`
|
||||
Zoneinfo sql.NullString `json:"zoneinfo"`
|
||||
Locale sql.NullString `json:"locale"`
|
||||
}
|
||||
|
|
|
@ -4,25 +4,33 @@ package database
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
CreateConfirmation(ctx context.Context, arg CreateConfirmationParams) (Confirmation, error)
|
||||
CountEmailsByIdentityID(ctx context.Context, identityID int64) (int64, error)
|
||||
CountUnverifiedEmailsByIdentityID(ctx context.Context, identityID int64) (int64, error)
|
||||
CreateEmail(ctx context.Context, arg CreateEmailParams) (Email, error)
|
||||
CreateReset(ctx context.Context, arg CreateResetParams) (Reset, error)
|
||||
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
|
||||
DestroyConfirmation(ctx context.Context, selector string) error
|
||||
CreateEmailConfirmation(ctx context.Context, arg CreateEmailConfirmationParams) (EmailConfirmation, error)
|
||||
CreateIdentity(ctx context.Context, arg CreateIdentityParams) (Identity, error)
|
||||
CreateReset(ctx context.Context, arg CreateResetParams) (PasswordReset, error)
|
||||
DestroyEmail(ctx context.Context, address string) error
|
||||
DestroyEmailConfirmation(ctx context.Context, emailAddress string) error
|
||||
DestroyReset(ctx context.Context, selector string) error
|
||||
GetConfirmationBySelector(ctx context.Context, selector string) (Confirmation, error)
|
||||
GetConfirmationByUserID(ctx context.Context, userID int64) (Confirmation, error)
|
||||
GetEmailByAddress(ctx context.Context, address string) (Email, error)
|
||||
GetEmailByUserID(ctx context.Context, userID int64) (Email, error)
|
||||
GetResetBySelector(ctx context.Context, selector string) (Reset, error)
|
||||
GetResetByUserID(ctx context.Context, userID int64) (Reset, error)
|
||||
GetUserByID(ctx context.Context, id int64) (User, error)
|
||||
UpdateEmail(ctx context.Context, arg UpdateEmailParams) error
|
||||
UpdateUser(ctx context.Context, arg UpdateUserParams) error
|
||||
GetEmailByIdentityID(ctx context.Context, identityID int64) ([]Email, error)
|
||||
GetEmailConfirmationByAddress(ctx context.Context, emailAddress string) (EmailConfirmation, error)
|
||||
GetEmailConfirmationBySelector(ctx context.Context, selector string) (EmailConfirmation, error)
|
||||
GetIdentityByID(ctx context.Context, id int64) (Identity, error)
|
||||
GetIdentityByLogin(ctx context.Context, login sql.NullString) (Identity, error)
|
||||
GetIdentityByPrimaryEmail(ctx context.Context, address string) (GetIdentityByPrimaryEmailRow, error)
|
||||
GetPrimaryEmailByIdentityID(ctx context.Context, identityID int64) (Email, error)
|
||||
GetResetByIdentityID(ctx context.Context, identityID int64) (PasswordReset, error)
|
||||
GetResetBySelector(ctx context.Context, selector string) (PasswordReset, error)
|
||||
UpdateEmailPrimary(ctx context.Context, arg UpdateEmailPrimaryParams) error
|
||||
UpdateEmailVerified(ctx context.Context, address string) error
|
||||
UpdateIdentityLogin(ctx context.Context, id int64) error
|
||||
UpdateIdentityPassphrase(ctx context.Context, id int64) error
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
|
|
|
@ -9,39 +9,45 @@ import (
|
|||
)
|
||||
|
||||
const createReset = `-- name: CreateReset :one
|
||||
INSERT INTO "reset" (
|
||||
"user_id", "selector", "verifier", "expires_at"
|
||||
INSERT INTO "password_reset" (
|
||||
"identity_id",
|
||||
"selector",
|
||||
"verifier",
|
||||
"valid_until"
|
||||
) VALUES (
|
||||
$1, $2, $3, $4
|
||||
) RETURNING user_id, selector, verifier, expires_at
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4
|
||||
) RETURNING identity_id, selector, verifier, valid_until
|
||||
`
|
||||
|
||||
type CreateResetParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Selector string `json:"selector"`
|
||||
Verifier []byte `json:"verifier"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
IdentityID int64 `json:"identity_id"`
|
||||
Selector string `json:"selector"`
|
||||
Verifier []byte `json:"verifier"`
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateReset(ctx context.Context, arg CreateResetParams) (Reset, error) {
|
||||
func (q *Queries) CreateReset(ctx context.Context, arg CreateResetParams) (PasswordReset, error) {
|
||||
row := q.db.QueryRowContext(ctx, createReset,
|
||||
arg.UserID,
|
||||
arg.IdentityID,
|
||||
arg.Selector,
|
||||
arg.Verifier,
|
||||
arg.ExpiresAt,
|
||||
arg.ValidUntil,
|
||||
)
|
||||
var i Reset
|
||||
var i PasswordReset
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.IdentityID,
|
||||
&i.Selector,
|
||||
&i.Verifier,
|
||||
&i.ExpiresAt,
|
||||
&i.ValidUntil,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const destroyReset = `-- name: DestroyReset :exec
|
||||
DELETE FROM "reset" WHERE "selector" = $1
|
||||
DELETE FROM "password_reset" WHERE "selector" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DestroyReset(ctx context.Context, selector string) error {
|
||||
|
@ -49,40 +55,40 @@ func (q *Queries) DestroyReset(ctx context.Context, selector string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
const getResetByIdentityID = `-- name: GetResetByIdentityID :one
|
||||
SELECT
|
||||
identity_id, selector, verifier, valid_until
|
||||
FROM "password_reset"
|
||||
WHERE "identity_id" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetResetByIdentityID(ctx context.Context, identityID int64) (PasswordReset, error) {
|
||||
row := q.db.QueryRowContext(ctx, getResetByIdentityID, identityID)
|
||||
var i PasswordReset
|
||||
err := row.Scan(
|
||||
&i.IdentityID,
|
||||
&i.Selector,
|
||||
&i.Verifier,
|
||||
&i.ValidUntil,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getResetBySelector = `-- name: GetResetBySelector :one
|
||||
SELECT
|
||||
"user_id", "selector", "verifier", "expires_at"
|
||||
FROM "reset"
|
||||
identity_id, selector, verifier, valid_until
|
||||
FROM "password_reset"
|
||||
WHERE "selector" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetResetBySelector(ctx context.Context, selector string) (Reset, error) {
|
||||
func (q *Queries) GetResetBySelector(ctx context.Context, selector string) (PasswordReset, error) {
|
||||
row := q.db.QueryRowContext(ctx, getResetBySelector, selector)
|
||||
var i Reset
|
||||
var i PasswordReset
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.IdentityID,
|
||||
&i.Selector,
|
||||
&i.Verifier,
|
||||
&i.ExpiresAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getResetByUserID = `-- name: GetResetByUserID :one
|
||||
SELECT
|
||||
"user_id", "selector", "verifier", "expires_at"
|
||||
FROM "reset"
|
||||
WHERE "user_id" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetResetByUserID(ctx context.Context, userID int64) (Reset, error) {
|
||||
row := q.db.QueryRowContext(ctx, getResetByUserID, userID)
|
||||
var i Reset
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.Selector,
|
||||
&i.Verifier,
|
||||
&i.ExpiresAt,
|
||||
&i.ValidUntil,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
-- name: CreateConfirmation :one
|
||||
INSERT INTO "public"."confirmation" (
|
||||
"email_address", "user_id", "selector", "verifier", "expires_at"
|
||||
-- name: CreateEmailConfirmation :one
|
||||
INSERT INTO "email_confirmation" (
|
||||
"selector",
|
||||
"verifier",
|
||||
"valid_until",
|
||||
"email_address"
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4
|
||||
) RETURNING *;
|
||||
|
||||
-- name: DestroyConfirmation :exec
|
||||
DELETE FROM "public"."confirmation" WHERE "selector" = $1;
|
||||
|
||||
-- name: GetConfirmationBySelector :one
|
||||
-- name: GetEmailConfirmationBySelector :one
|
||||
SELECT
|
||||
"email_address", "user_id", "selector", "verifier", "expires_at"
|
||||
FROM "public"."confirmation"
|
||||
WHERE "selector" = $1;
|
||||
*
|
||||
FROM "email_confirmation"
|
||||
WHERE "selector" = $1
|
||||
LIMIT 1;
|
||||
|
||||
-- name: GetConfirmationByUserID :one
|
||||
-- name: GetEmailConfirmationByAddress :one
|
||||
SELECT
|
||||
"email_address", "user_id", "selector", "verifier", "expires_at"
|
||||
FROM "public"."confirmation"
|
||||
WHERE "user_id" = $1;
|
||||
*
|
||||
FROM "email_confirmation"
|
||||
WHERE "email_address" = $1
|
||||
LIMIT 1;
|
||||
|
||||
-- name: DestroyEmailConfirmation :exec
|
||||
DELETE FROM "email_confirmation" WHERE "email_address" = $1;
|
|
@ -1,28 +1,71 @@
|
|||
-- name: CreateEmail :one
|
||||
INSERT INTO "email" (
|
||||
"address", "user_id", "created_at"
|
||||
"address",
|
||||
"identity_id",
|
||||
"is_verified"
|
||||
) VALUES (
|
||||
$1, $2, $3
|
||||
$1, $2, $3
|
||||
) RETURNING *;
|
||||
|
||||
-- name: UpdateEmail :exec
|
||||
-- name: UpdateEmailVerified :exec
|
||||
UPDATE "email" SET (
|
||||
"user_id", "created_at"
|
||||
"is_verified"
|
||||
) = (
|
||||
$1, $2
|
||||
) WHERE "address" = $3;
|
||||
$2
|
||||
) WHERE "address" = $1;
|
||||
|
||||
-- name: DestroyEmail :exec
|
||||
DELETE FROM "email" WHERE "address" = $1;
|
||||
-- name: UpdateEmailPrimary :exec
|
||||
-- UpdateEmailPrimary sets exactly one primary email for an identity.
|
||||
-- The mail address has to have been verified.
|
||||
-- this query basically combines these two queries into one atomic one:
|
||||
-- UPDATE "email" SET "is_primary" = false WHERE "identity_id" = $1;
|
||||
-- UPDATE "email" SET "is_primary" = true WHERE "identity_id" = $1 AND "address" = $2 AND "is_verified" = true;
|
||||
UPDATE "email"
|
||||
SET
|
||||
"is_primary" = NOT "is_primary"
|
||||
WHERE
|
||||
"identity_id" = $1 AND (
|
||||
( "address" <> $2 AND "is_primary" = true ) OR
|
||||
( "address" = $2 AND "is_primary" = false AND "is_verified" = true )
|
||||
);
|
||||
|
||||
-- name: GetEmailByAddress :one
|
||||
SELECT
|
||||
"address", "user_id", "created_at"
|
||||
FROM "public"."email"
|
||||
WHERE "address" = $1;
|
||||
*
|
||||
FROM "email"
|
||||
WHERE
|
||||
"address" = $1;
|
||||
|
||||
-- name: GetEmailByUserID :one
|
||||
-- name: GetEmailByIdentityID :many
|
||||
SELECT
|
||||
"address", "user_id", "created_at"
|
||||
FROM "public"."email"
|
||||
WHERE "user_id" = $1;
|
||||
*
|
||||
FROM "email"
|
||||
WHERE
|
||||
"identity_id" = $1;
|
||||
|
||||
-- name: GetPrimaryEmailByIdentityID :one
|
||||
SELECT
|
||||
*
|
||||
FROM "email"
|
||||
WHERE
|
||||
"identity_id" = $1 AND "is_primary";
|
||||
|
||||
-- name: CountEmailsByIdentityID :one
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
"email"
|
||||
WHERE
|
||||
"identity_id" = $1;
|
||||
|
||||
-- name: CountUnverifiedEmailsByIdentityID :one
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
"email"
|
||||
WHERE
|
||||
"identity_id" = $1 AND
|
||||
"is_verified" = FALSE;
|
||||
|
||||
-- name: DestroyEmail :exec
|
||||
DELETE FROM "email" WHERE "address" = $1;
|
48
internal/database/sql/queries/identity.sql
Normal file
48
internal/database/sql/queries/identity.sql
Normal file
|
@ -0,0 +1,48 @@
|
|||
-- name: CreateIdentity :one
|
||||
INSERT INTO "identity" (
|
||||
"login",
|
||||
"passphrase",
|
||||
"is_admin",
|
||||
"is_disabled"
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetIdentityByID :one
|
||||
SELECT * FROM "identity"
|
||||
WHERE "id" = $1
|
||||
LIMIT 1;
|
||||
|
||||
-- name: GetIdentityByLogin :one
|
||||
SELECT * FROM "identity"
|
||||
WHERE "login" = $1
|
||||
LIMIT 1;
|
||||
|
||||
-- name: GetIdentityByPrimaryEmail :one
|
||||
SELECT * FROM "identity"
|
||||
INNER JOIN "email"
|
||||
ON "email"."identity_id" = "identity"."id"
|
||||
WHERE
|
||||
"email"."address" = $1 AND
|
||||
"email"."is_primary"
|
||||
LIMIT 1;
|
||||
|
||||
-- name: UpdateIdentityPassphrase :exec
|
||||
UPDATE "identity" SET (
|
||||
"passphrase"
|
||||
) = (
|
||||
$2
|
||||
) WHERE "id" = $1;
|
||||
|
||||
-- name: UpdateIdentityLogin :exec
|
||||
UPDATE "identity" SET (
|
||||
"login"
|
||||
) = (
|
||||
$2
|
||||
) WHERE "id" = $1;
|
||||
|
||||
-- Name: DestroyIdentity :exec
|
||||
DELETE FROM "identity" WHERE "id" = $1;
|
41
internal/database/sql/queries/person.sql
Normal file
41
internal/database/sql/queries/person.sql
Normal file
|
@ -0,0 +1,41 @@
|
|||
-- Name: CreatePerson :one
|
||||
INSERT INTO "person" (
|
||||
"identity_id",
|
||||
"display_name",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"image_url",
|
||||
"zoneinfo",
|
||||
"locale"
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7
|
||||
) RETURNING *;
|
||||
|
||||
-- Name: GetPersonByIdentityID :one
|
||||
SELECT * FROM "person"
|
||||
WHERE
|
||||
"identity_id" = $1
|
||||
LIMIT 1;
|
||||
|
||||
-- Name: UpdatePerson :exec
|
||||
UPDATE "person" SET (
|
||||
"display_name",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"image_url",
|
||||
"zoneinfo",
|
||||
"locale"
|
||||
) = (
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7
|
||||
) WHERE "identity_id" = $1;
|
|
@ -1,21 +1,27 @@
|
|||
-- name: CreateReset :one
|
||||
INSERT INTO "reset" (
|
||||
"user_id", "selector", "verifier", "expires_at"
|
||||
INSERT INTO "password_reset" (
|
||||
"identity_id",
|
||||
"selector",
|
||||
"verifier",
|
||||
"valid_until"
|
||||
) VALUES (
|
||||
$1, $2, $3, $4
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4
|
||||
) RETURNING *;
|
||||
|
||||
-- name: DestroyReset :exec
|
||||
DELETE FROM "reset" WHERE "selector" = $1;
|
||||
DELETE FROM "password_reset" WHERE "selector" = $1;
|
||||
|
||||
-- name: GetResetBySelector :one
|
||||
SELECT
|
||||
"user_id", "selector", "verifier", "expires_at"
|
||||
FROM "reset"
|
||||
*
|
||||
FROM "password_reset"
|
||||
WHERE "selector" = $1;
|
||||
|
||||
-- name: GetResetByUserID :one
|
||||
-- name: GetResetByIdentityID :one
|
||||
SELECT
|
||||
"user_id", "selector", "verifier", "expires_at"
|
||||
FROM "reset"
|
||||
WHERE "user_id" = $1;
|
||||
*
|
||||
FROM "password_reset"
|
||||
WHERE "identity_id" = $1;
|
|
@ -1,19 +0,0 @@
|
|||
-- name: GetUserByID :one
|
||||
SELECT
|
||||
"id", "is_admin", "password", "created_at"
|
||||
FROM "user"
|
||||
WHERE "id" = $1;
|
||||
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO "user" (
|
||||
"is_admin", "password", "created_at"
|
||||
) VALUES (
|
||||
$1, $2, $3
|
||||
) RETURNING *;
|
||||
|
||||
-- name: UpdateUser :exec
|
||||
UPDATE "user" SET (
|
||||
"is_admin", "password", "created_at"
|
||||
) = (
|
||||
$1, $2, $3
|
||||
) WHERE "id" = $4;
|
|
@ -1,58 +1,122 @@
|
|||
-- This file is used for generating queries; If you change anything please
|
||||
-- also add migrations in `assets/migrations`.
|
||||
|
||||
CREATE TABLE "user" (
|
||||
"id" bigserial NOT NULL,
|
||||
"is_admin" boolean NOT NULL DEFAULT false,
|
||||
"password" bytea NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT NOW(),
|
||||
-- pgcrypto adds functions for generating UUIDs.
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
-- citext adds indexable case-insensitive text fields.
|
||||
CREATE EXTENSION IF NOT EXISTS "citext";
|
||||
|
||||
-- An Identity is any object that can participate as an actor in the system.
|
||||
-- It can have groups, permissions, own other objects etc.
|
||||
CREATE TABLE "identity" (
|
||||
"id" bigserial NOT NULL,
|
||||
"login" citext NULL,
|
||||
"passphrase" bytea NULL,
|
||||
"totp_secret" text NULL,
|
||||
"is_admin" boolean NOT NULL DEFAULT false,
|
||||
"is_disabled" boolean NOT NULL DEFAULT false,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE UNIQUE INDEX "identity_login_key" ON "identity" ("login");
|
||||
|
||||
-- A person is a human actor within the system, it is linked to exactly one
|
||||
-- identity.
|
||||
CREATE TABLE "person" (
|
||||
"identity_id" bigint NOT NULL,
|
||||
"display_name" text NULL,
|
||||
"first_name" text NULL,
|
||||
"last_name" text NULL,
|
||||
"image_url" text NULL,
|
||||
"zoneinfo" text NULL,
|
||||
"locale" text NULL,
|
||||
FOREIGN KEY ("identity_id")
|
||||
REFERENCES "identity" ("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE RESTRICT,
|
||||
PRIMARY KEY ("identity_id")
|
||||
);
|
||||
|
||||
-- Email is an email address for an identity (most likely for a person),
|
||||
-- that may be verified. Zero or one email address assigned to the identity
|
||||
-- may be "primary", e.g. used for notifications or login.
|
||||
CREATE TABLE "email" (
|
||||
"address" text NOT NULL,
|
||||
"user_id" bigint NOT NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT NOW(),
|
||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
"address" citext NOT NULL,
|
||||
"identity_id" bigint NOT NULL,
|
||||
"is_verified" boolean NOT NULL DEFAULT false,
|
||||
"is_primary" boolean NOT NULL DEFAULT false,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
FOREIGN KEY ("identity_id")
|
||||
REFERENCES "identity" ("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE RESTRICT,
|
||||
PRIMARY KEY ("address")
|
||||
);
|
||||
CREATE INDEX ON "email" ("user_id");
|
||||
CREATE INDEX "email_is_verified_idx" ON "email" ("is_verified")
|
||||
WHERE "is_verified" = true;
|
||||
CREATE INDEX "email_identity_id_idx" ON "email" ("identity_id");
|
||||
CREATE UNIQUE INDEX "email_is_primary_key" ON "email" ("identity_id", "is_primary")
|
||||
WHERE "is_primary" = true;
|
||||
|
||||
CREATE TABLE "confirmation" (
|
||||
"email_address" text NOT NULL,
|
||||
"user_id" bigint NOT NULL,
|
||||
"selector" text NOT NULL,
|
||||
"verifier" bytea NOT NULL, -- hashed
|
||||
"expires_at" timestamptz NOT NULL DEFAULT NOW(),
|
||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
PRIMARY KEY ("selector")
|
||||
-- Email Confirmation tracks all email confirmations that have been sent out.
|
||||
CREATE TABLE "email_confirmation" (
|
||||
"email_address" citext NOT NULL,
|
||||
"selector" text NOT NULL,
|
||||
"verifier" bytea NOT NULL,
|
||||
"valid_until" timestamptz NOT NULL,
|
||||
FOREIGN KEY ("email_address")
|
||||
REFERENCES "email" ("address")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE RESTRICT,
|
||||
PRIMARY KEY ("email_address")
|
||||
);
|
||||
CREATE INDEX ON "confirmation" ("user_id");
|
||||
CREATE UNIQUE INDEX "email_confirmation_selector_key"
|
||||
ON "email_confirmation" ("selector");
|
||||
|
||||
CREATE TABLE "reset" (
|
||||
"user_id" bigint NOT NULL,
|
||||
"selector" text NOT NULL,
|
||||
"verifier" bytea NOT NULL, -- hashed
|
||||
"expires_at" timestamptz NOT NULL DEFAULT NOW(),
|
||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
PRIMARY KEY ("selector")
|
||||
-- Password reset keeps track of the password reset tokens.
|
||||
CREATE TABLE "password_reset" (
|
||||
"identity_id" bigserial NOT NULL,
|
||||
"selector" text NOT NULL,
|
||||
"verifier" bytea NOT NULL,
|
||||
"valid_until" timestamptz NOT NULL,
|
||||
FOREIGN KEY ("identity_id")
|
||||
REFERENCES "person" ("identity_id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE RESTRICT,
|
||||
PRIMARY KEY ("identity_id")
|
||||
);
|
||||
CREATE UNIQUE INDEX ON "reset" ("user_id");
|
||||
CREATE UNIQUE INDEX "password_reset_selector_key" ON "password_reset" ("selector");
|
||||
|
||||
CREATE TABLE "external_auth" (
|
||||
"id" bigserial NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"config" jsonb NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
"name" text NOT NULL,
|
||||
"oidc_url" text NULL,
|
||||
"auth_url" text NOT NULL,
|
||||
"token_url" text NOT NULL,
|
||||
"client_key" text NOT NULL,
|
||||
"client_secret" text NOT NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY ("name")
|
||||
);
|
||||
CREATE INDEX ON "external_auth" ("type");
|
||||
CREATE UNIQUE INDEX "external_auth_name_key" ON "external_auth" ("name");
|
||||
|
||||
CREATE TABLE "external_user" (
|
||||
"external_auth_id" bigint NOT NULL,
|
||||
"foreign_id" text NOT NULL,
|
||||
"user_id" bigint NOT NULL,
|
||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("external_auth_id") REFERENCES "external_auth" ("id") ON DELETE CASCADE,
|
||||
PRIMARY KEY ("external_auth_id", "foreign_id")
|
||||
"identity_id" bigint NOT NULL,
|
||||
"external_auth_name" text NOT NULL,
|
||||
"external_id" text NOT NULL,
|
||||
"auth_token" text NULL,
|
||||
"refresh_token" text NULL,
|
||||
"identity_token" text NULL,
|
||||
FOREIGN KEY ("identity_id")
|
||||
REFERENCES "identity" ("id")
|
||||
ON UPDATE RESTRICT
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY ("external_auth_name")
|
||||
REFERENCES "external_auth" ("name")
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
PRIMARY KEY ("identity_id")
|
||||
);
|
||||
CREATE INDEX "external_user_external_auth_name_idx"
|
||||
ON "external_user" ("external_auth_name");
|
||||
CREATE UNIQUE INDEX "external_user_external_id_key"
|
||||
ON "external_user" ("external_auth_name", "external_id");
|
|
@ -1,79 +0,0 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// source: user.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO "user" (
|
||||
"is_admin", "password", "created_at"
|
||||
) VALUES (
|
||||
$1, $2, $3
|
||||
) RETURNING id, is_admin, password, created_at
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Password []byte `json:"password"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, createUser, arg.IsAdmin, arg.Password, arg.CreatedAt)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.IsAdmin,
|
||||
&i.Password,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT
|
||||
"id", "is_admin", "password", "created_at"
|
||||
FROM "user"
|
||||
WHERE "id" = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserByID, id)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.IsAdmin,
|
||||
&i.Password,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUser = `-- name: UpdateUser :exec
|
||||
UPDATE "user" SET (
|
||||
"is_admin", "password", "created_at"
|
||||
) = (
|
||||
$1, $2, $3
|
||||
) WHERE "id" = $4
|
||||
`
|
||||
|
||||
type UpdateUserParams struct {
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Password []byte `json:"password"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateUser,
|
||||
arg.IsAdmin,
|
||||
arg.Password,
|
||||
arg.CreatedAt,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
ldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// TimeLimitSeconds is the maximal time that LDAP will spend on a single
|
||||
// request.
|
||||
TimeLimitSeconds = 5
|
||||
// SizeLimitEntries is the biggest number of results that is returned from a
|
||||
// search request.
|
||||
SizeLimitEntries = 100
|
||||
// UserAttributes is the list of LDAP-Attributes that will be used for user
|
||||
// accounts.
|
||||
UserAttributes = []string{
|
||||
"dn", // distinguished name, the unique "path" to a LDAP entry.
|
||||
"cn", // common name, human readable e.g. "Max Powers".
|
||||
"uid", // user identified, same as the username/login name.
|
||||
"uidNumber", // unique user ID, integer.
|
||||
"createTimestamp", // LDAP timestamp of when this entry was created.
|
||||
"modifyTimestamp", // LDAP timestemp of when this entry was last modified.
|
||||
}
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Host string
|
||||
Port int
|
||||
|
||||
bindDN string
|
||||
bindPW string
|
||||
|
||||
userBaseDN string
|
||||
}
|
||||
|
||||
func (s *Server) newConn() (*ldap.Conn, error) {
|
||||
lc, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to dial LDAP")
|
||||
}
|
||||
|
||||
err = lc.Bind(s.bindDN, s.bindPW)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to bind service account to LDAP")
|
||||
}
|
||||
|
||||
return lc, nil
|
||||
}
|
||||
|
||||
// buildFilterForID builds an LDAP filter that searches for a user with a
|
||||
// specific uidNumber.
|
||||
func (s *Server) buildFilterForUserID(id int) string {
|
||||
return fmt.Sprintf("(&(objectClass=inetOrgPerson)(uidNumber=%d))", id)
|
||||
}
|
||||
|
||||
func (s *Server) buildFilterForEmail(email string) string {
|
||||
reg := regexp.MustCompile("[^a-zA-Z0-9-+._@]+")
|
||||
email = reg.ReplaceAllString(email, "")
|
||||
return fmt.Sprintf("(&(objectClass=)())")
|
||||
|
||||
// Conn is an LDAP connection.
|
||||
type Conn struct {
|
||||
*ldap.Conn
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Entry *ldap.Entry
|
||||
}
|
||||
|
||||
// GetDisplayName implements User interface by returning the display name.
|
||||
func (u User) GetDisplayName() string {
|
||||
display := u.Entry.GetAttributeValue("displayName")
|
||||
|
||||
if display == "" {
|
||||
display = u.Entry.GetAttributeValue("givenName")
|
||||
}
|
||||
|
||||
if display == "" {
|
||||
display = u.Entry.GetAttributeValue("cn")
|
||||
}
|
||||
|
||||
if display == "" {
|
||||
display = u.GetID()
|
||||
}
|
||||
return display
|
||||
}
|
||||
|
||||
// GetID implements the User interface by returning the user ID.
|
||||
func (u User) GetID() string {
|
||||
id := u.Entry.GetAttributeValue("uid")
|
||||
return id
|
||||
}
|
||||
|
||||
func (lc *Conn) UserByID(ID string) (User, error) {
|
||||
ldap.NewSearchRequest()
|
||||
}
|
||||
|
||||
func (s *Server) UserByEmail(email string) (User, error) {
|
||||
lc, err := s.newConn()
|
||||
ldap.NewSearchRequest(
|
||||
s.userBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
|
||||
SizeLimitEntries, TimeLimitSeconds, false,
|
||||
s.buildFilterForEmail(email), UserAttributes, nil)
|
||||
}
|
Loading…
Reference in a new issue