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
|
migrate: .env
|
||||||
$(GORUN) --tags=dev main.go migrate -u
|
$(GORUN) --tags=dev main.go migrate -u
|
||||||
|
|
||||||
.PHONY: migratedown
|
.PHONY: drop
|
||||||
migratedown: .env
|
drop: .env
|
||||||
$(GORUN) --tags=dev main.go migrate --revision=0
|
$(GORUN) --tags=dev main.go migrate --drop-all
|
||||||
|
|
||||||
.PHONY: generate
|
.PHONY: generate
|
||||||
generate:
|
generate:
|
||||||
|
|
|
@ -1 +1,6 @@
|
||||||
-- this file is only here so the database can track a completely empty state.
|
-- 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 "email";
|
||||||
DROP TABLE "user";
|
|
||||||
|
DROP TABLE "person";
|
||||||
|
|
||||||
|
DROP TABLE "identity";
|
||||||
|
|
|
@ -1,37 +1,51 @@
|
||||||
CREATE TABLE "user" (
|
-- An Identity is any object that can participate as an actor in the system.
|
||||||
"id" bigserial NOT NULL,
|
-- It can have groups, permissions, own other objects etc.
|
||||||
"is_admin" boolean NOT NULL DEFAULT false,
|
CREATE TABLE "identity" (
|
||||||
"password" bytea NULL,
|
"id" bigserial NOT NULL,
|
||||||
"created_at" timestamptz NOT NULL DEFAULT NOW(),
|
"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")
|
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" (
|
CREATE TABLE "email" (
|
||||||
"address" text NOT NULL,
|
"address" citext NOT NULL,
|
||||||
"user_id" bigint NOT NULL,
|
"identity_id" bigint NOT NULL,
|
||||||
"created_at" timestamptz NOT NULL DEFAULT NOW(),
|
"is_verified" boolean NOT NULL DEFAULT false,
|
||||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
"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")
|
PRIMARY KEY ("address")
|
||||||
);
|
);
|
||||||
CREATE INDEX ON "email" ("user_id");
|
CREATE INDEX "email_is_verified_idx" ON "email" ("is_verified")
|
||||||
|
WHERE "is_verified" = true;
|
||||||
CREATE TABLE "confirmation" (
|
CREATE INDEX "email_identity_id_idx" ON "email" ("identity_id");
|
||||||
"email_address" text NOT NULL,
|
CREATE UNIQUE INDEX "email_is_primary_key" ON "email" ("identity_id", "is_primary")
|
||||||
"user_id" bigint NOT NULL,
|
WHERE "is_primary" = true;
|
||||||
"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");
|
|
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"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const createConfirmation = `-- name: CreateConfirmation :one
|
const createEmailConfirmation = `-- name: CreateEmailConfirmation :one
|
||||||
INSERT INTO "public"."confirmation" (
|
INSERT INTO "email_confirmation" (
|
||||||
"email_address", "user_id", "selector", "verifier", "expires_at"
|
"selector",
|
||||||
|
"verifier",
|
||||||
|
"valid_until",
|
||||||
|
"email_address"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5
|
$1,
|
||||||
) RETURNING email_address, user_id, selector, verifier, expires_at
|
$2,
|
||||||
|
$3,
|
||||||
|
$4
|
||||||
|
) RETURNING email_address, selector, verifier, valid_until
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateConfirmationParams struct {
|
type CreateEmailConfirmationParams struct {
|
||||||
EmailAddress string `json:"email_address"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
Selector string `json:"selector"`
|
Selector string `json:"selector"`
|
||||||
Verifier []byte `json:"verifier"`
|
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) {
|
func (q *Queries) CreateEmailConfirmation(ctx context.Context, arg CreateEmailConfirmationParams) (EmailConfirmation, error) {
|
||||||
row := q.db.QueryRowContext(ctx, createConfirmation,
|
row := q.db.QueryRowContext(ctx, createEmailConfirmation,
|
||||||
arg.EmailAddress,
|
|
||||||
arg.UserID,
|
|
||||||
arg.Selector,
|
arg.Selector,
|
||||||
arg.Verifier,
|
arg.Verifier,
|
||||||
arg.ExpiresAt,
|
arg.ValidUntil,
|
||||||
|
arg.EmailAddress,
|
||||||
)
|
)
|
||||||
var i Confirmation
|
var i EmailConfirmation
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.EmailAddress,
|
&i.EmailAddress,
|
||||||
&i.UserID,
|
|
||||||
&i.Selector,
|
&i.Selector,
|
||||||
&i.Verifier,
|
&i.Verifier,
|
||||||
&i.ExpiresAt,
|
&i.ValidUntil,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const destroyConfirmation = `-- name: DestroyConfirmation :exec
|
const destroyEmailConfirmation = `-- name: DestroyEmailConfirmation :exec
|
||||||
DELETE FROM "public"."confirmation" WHERE "selector" = $1
|
DELETE FROM "email_confirmation" WHERE "email_address" = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) DestroyConfirmation(ctx context.Context, selector string) error {
|
func (q *Queries) DestroyEmailConfirmation(ctx context.Context, emailAddress string) error {
|
||||||
_, err := q.db.ExecContext(ctx, destroyConfirmation, selector)
|
_, err := q.db.ExecContext(ctx, destroyEmailConfirmation, emailAddress)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getConfirmationBySelector = `-- name: GetConfirmationBySelector :one
|
const getEmailConfirmationByAddress = `-- name: GetEmailConfirmationByAddress :one
|
||||||
SELECT
|
SELECT
|
||||||
"email_address", "user_id", "selector", "verifier", "expires_at"
|
email_address, selector, verifier, valid_until
|
||||||
FROM "public"."confirmation"
|
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
|
WHERE "selector" = $1
|
||||||
|
LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetConfirmationBySelector(ctx context.Context, selector string) (Confirmation, error) {
|
func (q *Queries) GetEmailConfirmationBySelector(ctx context.Context, selector string) (EmailConfirmation, error) {
|
||||||
row := q.db.QueryRowContext(ctx, getConfirmationBySelector, selector)
|
row := q.db.QueryRowContext(ctx, getEmailConfirmationBySelector, selector)
|
||||||
var i Confirmation
|
var i EmailConfirmation
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.EmailAddress,
|
&i.EmailAddress,
|
||||||
&i.UserID,
|
|
||||||
&i.Selector,
|
&i.Selector,
|
||||||
&i.Verifier,
|
&i.Verifier,
|
||||||
&i.ExpiresAt,
|
&i.ValidUntil,
|
||||||
)
|
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,27 +5,67 @@ package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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
|
const createEmail = `-- name: CreateEmail :one
|
||||||
INSERT INTO "email" (
|
INSERT INTO "email" (
|
||||||
"address", "user_id", "created_at"
|
"address",
|
||||||
|
"identity_id",
|
||||||
|
"is_verified"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3
|
$1, $2, $3
|
||||||
) RETURNING address, user_id, created_at
|
) RETURNING address, identity_id, is_verified, is_primary, created_at
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateEmailParams struct {
|
type CreateEmailParams struct {
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
UserID int64 `json:"user_id"`
|
IdentityID int64 `json:"identity_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
IsVerified bool `json:"is_verified"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateEmail(ctx context.Context, arg CreateEmailParams) (Email, error) {
|
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
|
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
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,47 +80,118 @@ func (q *Queries) DestroyEmail(ctx context.Context, address string) error {
|
||||||
|
|
||||||
const getEmailByAddress = `-- name: GetEmailByAddress :one
|
const getEmailByAddress = `-- name: GetEmailByAddress :one
|
||||||
SELECT
|
SELECT
|
||||||
"address", "user_id", "created_at"
|
address, identity_id, is_verified, is_primary, created_at
|
||||||
FROM "public"."email"
|
FROM "email"
|
||||||
WHERE "address" = $1
|
WHERE
|
||||||
|
"address" = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetEmailByAddress(ctx context.Context, address string) (Email, error) {
|
func (q *Queries) GetEmailByAddress(ctx context.Context, address string) (Email, error) {
|
||||||
row := q.db.QueryRowContext(ctx, getEmailByAddress, address)
|
row := q.db.QueryRowContext(ctx, getEmailByAddress, address)
|
||||||
var i Email
|
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
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEmailByUserID = `-- name: GetEmailByUserID :one
|
const getEmailByIdentityID = `-- name: GetEmailByIdentityID :many
|
||||||
SELECT
|
SELECT
|
||||||
"address", "user_id", "created_at"
|
address, identity_id, is_verified, is_primary, created_at
|
||||||
FROM "public"."email"
|
FROM "email"
|
||||||
WHERE "user_id" = $1
|
WHERE
|
||||||
|
"identity_id" = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetEmailByUserID(ctx context.Context, userID int64) (Email, error) {
|
func (q *Queries) GetEmailByIdentityID(ctx context.Context, identityID int64) ([]Email, error) {
|
||||||
row := q.db.QueryRowContext(ctx, getEmailByUserID, userID)
|
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
|
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
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateEmail = `-- name: UpdateEmail :exec
|
const updateEmailPrimary = `-- name: UpdateEmailPrimary :exec
|
||||||
UPDATE "email" SET (
|
UPDATE "email"
|
||||||
"user_id", "created_at"
|
SET
|
||||||
) = (
|
"is_primary" = NOT "is_primary"
|
||||||
$1, $2
|
WHERE
|
||||||
) WHERE "address" = $3
|
"identity_id" = $1 AND (
|
||||||
|
( "address" <> $2 AND "is_primary" = true ) OR
|
||||||
|
( "address" = $2 AND "is_primary" = false AND "is_verified" = true )
|
||||||
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateEmailParams struct {
|
type UpdateEmailPrimaryParams struct {
|
||||||
UserID int64 `json:"user_id"`
|
IdentityID int64 `json:"identity_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Address string `json:"address"`
|
||||||
Address string `json:"address"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateEmail(ctx context.Context, arg UpdateEmailParams) error {
|
// UpdateEmailPrimary sets exactly one primary email for an identity.
|
||||||
_, err := q.db.ExecContext(ctx, updateEmail, arg.UserID, arg.CreatedAt, arg.Address)
|
// 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
|
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
|
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
|
// Migrate Migrates the schema of the supplied Database. Supported
|
||||||
// methods are:
|
// methods are:
|
||||||
// * database.MigrateUp – Migrate to the latest version
|
// * database.MigrateUp – Migrate to the latest version
|
||||||
|
|
|
@ -3,47 +3,67 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Confirmation struct {
|
type Email struct {
|
||||||
EmailAddress string `json:"email_address"`
|
Address string `json:"address"`
|
||||||
UserID int64 `json:"user_id"`
|
IdentityID int64 `json:"identity_id"`
|
||||||
Selector string `json:"selector"`
|
IsVerified bool `json:"is_verified"`
|
||||||
Verifier []byte `json:"verifier"`
|
IsPrimary bool `json:"is_primary"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Email struct {
|
type EmailConfirmation struct {
|
||||||
Address string `json:"address"`
|
EmailAddress string `json:"email_address"`
|
||||||
UserID int64 `json:"user_id"`
|
Selector string `json:"selector"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Verifier []byte `json:"verifier"`
|
||||||
|
ValidUntil time.Time `json:"valid_until"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExternalAuth struct {
|
type ExternalAuth struct {
|
||||||
ID int64 `json:"id"`
|
Name string `json:"name"`
|
||||||
Name string `json:"name"`
|
OidcUrl sql.NullString `json:"oidc_url"`
|
||||||
Type string `json:"type"`
|
AuthUrl string `json:"auth_url"`
|
||||||
Config json.RawMessage `json:"config"`
|
TokenUrl string `json:"token_url"`
|
||||||
|
ClientKey string `json:"client_key"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExternalUser struct {
|
type ExternalUser struct {
|
||||||
ExternalAuthID int64 `json:"external_auth_id"`
|
IdentityID int64 `json:"identity_id"`
|
||||||
ForeignID string `json:"foreign_id"`
|
ExternalAuthName string `json:"external_auth_name"`
|
||||||
UserID int64 `json:"user_id"`
|
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 {
|
type Identity struct {
|
||||||
UserID int64 `json:"user_id"`
|
ID int64 `json:"id"`
|
||||||
Selector string `json:"selector"`
|
Login sql.NullString `json:"login"`
|
||||||
Verifier []byte `json:"verifier"`
|
Passphrase []byte `json:"passphrase"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
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 {
|
type PasswordReset struct {
|
||||||
ID int64 `json:"id"`
|
IdentityID int64 `json:"identity_id"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
Selector string `json:"selector"`
|
||||||
Password []byte `json:"password"`
|
Verifier []byte `json:"verifier"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Querier interface {
|
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)
|
CreateEmail(ctx context.Context, arg CreateEmailParams) (Email, error)
|
||||||
CreateReset(ctx context.Context, arg CreateResetParams) (Reset, error)
|
CreateEmailConfirmation(ctx context.Context, arg CreateEmailConfirmationParams) (EmailConfirmation, error)
|
||||||
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
|
CreateIdentity(ctx context.Context, arg CreateIdentityParams) (Identity, error)
|
||||||
DestroyConfirmation(ctx context.Context, selector string) error
|
CreateReset(ctx context.Context, arg CreateResetParams) (PasswordReset, error)
|
||||||
DestroyEmail(ctx context.Context, address string) error
|
DestroyEmail(ctx context.Context, address string) error
|
||||||
|
DestroyEmailConfirmation(ctx context.Context, emailAddress string) error
|
||||||
DestroyReset(ctx context.Context, selector 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)
|
GetEmailByAddress(ctx context.Context, address string) (Email, error)
|
||||||
GetEmailByUserID(ctx context.Context, userID int64) (Email, error)
|
GetEmailByIdentityID(ctx context.Context, identityID int64) ([]Email, error)
|
||||||
GetResetBySelector(ctx context.Context, selector string) (Reset, error)
|
GetEmailConfirmationByAddress(ctx context.Context, emailAddress string) (EmailConfirmation, error)
|
||||||
GetResetByUserID(ctx context.Context, userID int64) (Reset, error)
|
GetEmailConfirmationBySelector(ctx context.Context, selector string) (EmailConfirmation, error)
|
||||||
GetUserByID(ctx context.Context, id int64) (User, error)
|
GetIdentityByID(ctx context.Context, id int64) (Identity, error)
|
||||||
UpdateEmail(ctx context.Context, arg UpdateEmailParams) error
|
GetIdentityByLogin(ctx context.Context, login sql.NullString) (Identity, error)
|
||||||
UpdateUser(ctx context.Context, arg UpdateUserParams) 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)
|
var _ Querier = (*Queries)(nil)
|
||||||
|
|
|
@ -9,39 +9,45 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const createReset = `-- name: CreateReset :one
|
const createReset = `-- name: CreateReset :one
|
||||||
INSERT INTO "reset" (
|
INSERT INTO "password_reset" (
|
||||||
"user_id", "selector", "verifier", "expires_at"
|
"identity_id",
|
||||||
|
"selector",
|
||||||
|
"verifier",
|
||||||
|
"valid_until"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4
|
$1,
|
||||||
) RETURNING user_id, selector, verifier, expires_at
|
$2,
|
||||||
|
$3,
|
||||||
|
$4
|
||||||
|
) RETURNING identity_id, selector, verifier, valid_until
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateResetParams struct {
|
type CreateResetParams struct {
|
||||||
UserID int64 `json:"user_id"`
|
IdentityID int64 `json:"identity_id"`
|
||||||
Selector string `json:"selector"`
|
Selector string `json:"selector"`
|
||||||
Verifier []byte `json:"verifier"`
|
Verifier []byte `json:"verifier"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
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,
|
row := q.db.QueryRowContext(ctx, createReset,
|
||||||
arg.UserID,
|
arg.IdentityID,
|
||||||
arg.Selector,
|
arg.Selector,
|
||||||
arg.Verifier,
|
arg.Verifier,
|
||||||
arg.ExpiresAt,
|
arg.ValidUntil,
|
||||||
)
|
)
|
||||||
var i Reset
|
var i PasswordReset
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.UserID,
|
&i.IdentityID,
|
||||||
&i.Selector,
|
&i.Selector,
|
||||||
&i.Verifier,
|
&i.Verifier,
|
||||||
&i.ExpiresAt,
|
&i.ValidUntil,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const destroyReset = `-- name: DestroyReset :exec
|
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 {
|
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
|
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
|
const getResetBySelector = `-- name: GetResetBySelector :one
|
||||||
SELECT
|
SELECT
|
||||||
"user_id", "selector", "verifier", "expires_at"
|
identity_id, selector, verifier, valid_until
|
||||||
FROM "reset"
|
FROM "password_reset"
|
||||||
WHERE "selector" = $1
|
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)
|
row := q.db.QueryRowContext(ctx, getResetBySelector, selector)
|
||||||
var i Reset
|
var i PasswordReset
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.UserID,
|
&i.IdentityID,
|
||||||
&i.Selector,
|
&i.Selector,
|
||||||
&i.Verifier,
|
&i.Verifier,
|
||||||
&i.ExpiresAt,
|
&i.ValidUntil,
|
||||||
)
|
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,29 @@
|
||||||
-- name: CreateConfirmation :one
|
-- name: CreateEmailConfirmation :one
|
||||||
INSERT INTO "public"."confirmation" (
|
INSERT INTO "email_confirmation" (
|
||||||
"email_address", "user_id", "selector", "verifier", "expires_at"
|
"selector",
|
||||||
|
"verifier",
|
||||||
|
"valid_until",
|
||||||
|
"email_address"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4
|
||||||
) RETURNING *;
|
) RETURNING *;
|
||||||
|
|
||||||
-- name: DestroyConfirmation :exec
|
-- name: GetEmailConfirmationBySelector :one
|
||||||
DELETE FROM "public"."confirmation" WHERE "selector" = $1;
|
|
||||||
|
|
||||||
-- name: GetConfirmationBySelector :one
|
|
||||||
SELECT
|
SELECT
|
||||||
"email_address", "user_id", "selector", "verifier", "expires_at"
|
*
|
||||||
FROM "public"."confirmation"
|
FROM "email_confirmation"
|
||||||
WHERE "selector" = $1;
|
WHERE "selector" = $1
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
-- name: GetConfirmationByUserID :one
|
-- name: GetEmailConfirmationByAddress :one
|
||||||
SELECT
|
SELECT
|
||||||
"email_address", "user_id", "selector", "verifier", "expires_at"
|
*
|
||||||
FROM "public"."confirmation"
|
FROM "email_confirmation"
|
||||||
WHERE "user_id" = $1;
|
WHERE "email_address" = $1
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- name: DestroyEmailConfirmation :exec
|
||||||
|
DELETE FROM "email_confirmation" WHERE "email_address" = $1;
|
|
@ -1,28 +1,71 @@
|
||||||
-- name: CreateEmail :one
|
-- name: CreateEmail :one
|
||||||
INSERT INTO "email" (
|
INSERT INTO "email" (
|
||||||
"address", "user_id", "created_at"
|
"address",
|
||||||
|
"identity_id",
|
||||||
|
"is_verified"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3
|
$1, $2, $3
|
||||||
) RETURNING *;
|
) RETURNING *;
|
||||||
|
|
||||||
-- name: UpdateEmail :exec
|
-- name: UpdateEmailVerified :exec
|
||||||
UPDATE "email" SET (
|
UPDATE "email" SET (
|
||||||
"user_id", "created_at"
|
"is_verified"
|
||||||
) = (
|
) = (
|
||||||
$1, $2
|
$2
|
||||||
) WHERE "address" = $3;
|
) WHERE "address" = $1;
|
||||||
|
|
||||||
-- name: DestroyEmail :exec
|
-- name: UpdateEmailPrimary :exec
|
||||||
DELETE FROM "email" WHERE "address" = $1;
|
-- 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
|
-- name: GetEmailByAddress :one
|
||||||
SELECT
|
SELECT
|
||||||
"address", "user_id", "created_at"
|
*
|
||||||
FROM "public"."email"
|
FROM "email"
|
||||||
WHERE "address" = $1;
|
WHERE
|
||||||
|
"address" = $1;
|
||||||
|
|
||||||
-- name: GetEmailByUserID :one
|
-- name: GetEmailByIdentityID :many
|
||||||
SELECT
|
SELECT
|
||||||
"address", "user_id", "created_at"
|
*
|
||||||
FROM "public"."email"
|
FROM "email"
|
||||||
WHERE "user_id" = $1;
|
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
|
-- name: CreateReset :one
|
||||||
INSERT INTO "reset" (
|
INSERT INTO "password_reset" (
|
||||||
"user_id", "selector", "verifier", "expires_at"
|
"identity_id",
|
||||||
|
"selector",
|
||||||
|
"verifier",
|
||||||
|
"valid_until"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4
|
||||||
) RETURNING *;
|
) RETURNING *;
|
||||||
|
|
||||||
-- name: DestroyReset :exec
|
-- name: DestroyReset :exec
|
||||||
DELETE FROM "reset" WHERE "selector" = $1;
|
DELETE FROM "password_reset" WHERE "selector" = $1;
|
||||||
|
|
||||||
-- name: GetResetBySelector :one
|
-- name: GetResetBySelector :one
|
||||||
SELECT
|
SELECT
|
||||||
"user_id", "selector", "verifier", "expires_at"
|
*
|
||||||
FROM "reset"
|
FROM "password_reset"
|
||||||
WHERE "selector" = $1;
|
WHERE "selector" = $1;
|
||||||
|
|
||||||
-- name: GetResetByUserID :one
|
-- name: GetResetByIdentityID :one
|
||||||
SELECT
|
SELECT
|
||||||
"user_id", "selector", "verifier", "expires_at"
|
*
|
||||||
FROM "reset"
|
FROM "password_reset"
|
||||||
WHERE "user_id" = $1;
|
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
|
-- This file is used for generating queries; If you change anything please
|
||||||
-- also add migrations in `assets/migrations`.
|
-- also add migrations in `assets/migrations`.
|
||||||
|
|
||||||
CREATE TABLE "user" (
|
-- pgcrypto adds functions for generating UUIDs.
|
||||||
"id" bigserial NOT NULL,
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
"is_admin" boolean NOT NULL DEFAULT false,
|
-- citext adds indexable case-insensitive text fields.
|
||||||
"password" bytea NULL,
|
CREATE EXTENSION IF NOT EXISTS "citext";
|
||||||
"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")
|
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" (
|
CREATE TABLE "email" (
|
||||||
"address" text NOT NULL,
|
"address" citext NOT NULL,
|
||||||
"user_id" bigint NOT NULL,
|
"identity_id" bigint NOT NULL,
|
||||||
"created_at" timestamptz NOT NULL DEFAULT NOW(),
|
"is_verified" boolean NOT NULL DEFAULT false,
|
||||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
"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")
|
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 Confirmation tracks all email confirmations that have been sent out.
|
||||||
"email_address" text NOT NULL,
|
CREATE TABLE "email_confirmation" (
|
||||||
"user_id" bigint NOT NULL,
|
"email_address" citext NOT NULL,
|
||||||
"selector" text NOT NULL,
|
"selector" text NOT NULL,
|
||||||
"verifier" bytea NOT NULL, -- hashed
|
"verifier" bytea NOT NULL,
|
||||||
"expires_at" timestamptz NOT NULL DEFAULT NOW(),
|
"valid_until" timestamptz NOT NULL,
|
||||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
FOREIGN KEY ("email_address")
|
||||||
PRIMARY KEY ("selector")
|
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" (
|
-- Password reset keeps track of the password reset tokens.
|
||||||
"user_id" bigint NOT NULL,
|
CREATE TABLE "password_reset" (
|
||||||
"selector" text NOT NULL,
|
"identity_id" bigserial NOT NULL,
|
||||||
"verifier" bytea NOT NULL, -- hashed
|
"selector" text NOT NULL,
|
||||||
"expires_at" timestamptz NOT NULL DEFAULT NOW(),
|
"verifier" bytea NOT NULL,
|
||||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
"valid_until" timestamptz NOT NULL,
|
||||||
PRIMARY KEY ("selector")
|
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" (
|
CREATE TABLE "external_auth" (
|
||||||
"id" bigserial NOT NULL,
|
"name" text NOT NULL,
|
||||||
"name" text NOT NULL,
|
"oidc_url" text NULL,
|
||||||
"type" text NOT NULL,
|
"auth_url" text NOT NULL,
|
||||||
"config" jsonb NOT NULL,
|
"token_url" text NOT NULL,
|
||||||
PRIMARY KEY ("id")
|
"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" (
|
CREATE TABLE "external_user" (
|
||||||
"external_auth_id" bigint NOT NULL,
|
"identity_id" bigint NOT NULL,
|
||||||
"foreign_id" text NOT NULL,
|
"external_auth_name" text NOT NULL,
|
||||||
"user_id" bigint NOT NULL,
|
"external_id" text NOT NULL,
|
||||||
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
"auth_token" text NULL,
|
||||||
FOREIGN KEY ("external_auth_id") REFERENCES "external_auth" ("id") ON DELETE CASCADE,
|
"refresh_token" text NULL,
|
||||||
PRIMARY KEY ("external_auth_id", "foreign_id")
|
"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