extend database

This commit is contained in:
paul 2020-07-21 22:50:11 +02:00
parent 8e986c15b2
commit 6d25a0928f
25 changed files with 896 additions and 502 deletions

View file

@ -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:

View file

@ -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";

View file

@ -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";

View file

@ -1,37 +1,51 @@
CREATE TABLE "user" ( -- 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, "id" bigserial NOT NULL,
"login" citext NULL,
"passphrase" bytea NULL,
"totp_secret" text NULL,
"is_admin" boolean NOT NULL DEFAULT false, "is_admin" boolean NOT NULL DEFAULT false,
"password" bytea NULL, "is_disabled" boolean NOT NULL DEFAULT false,
"created_at" timestamptz NOT NULL DEFAULT NOW(), "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");

View file

@ -0,0 +1,3 @@
DROP TABLE "email_confirmation";
DROP TABLE "password_reset";

View 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");

View file

@ -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")
);

View 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");

View file

@ -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
} }

View file

@ -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 var i Email
err := row.Scan(&i.Address, &i.UserID, &i.CreatedAt) 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.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
} }

View 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
}

View file

@ -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

View file

@ -3,47 +3,67 @@
package database package database
import ( import (
"encoding/json" "database/sql"
"time" "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 { type Email struct {
Address string `json:"address"` Address string `json:"address"`
UserID int64 `json:"user_id"` IdentityID int64 `json:"identity_id"`
IsVerified bool `json:"is_verified"`
IsPrimary bool `json:"is_primary"`
CreatedAt time.Time `json:"created_at"` 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 { type ExternalAuth struct {
ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` OidcUrl sql.NullString `json:"oidc_url"`
Config json.RawMessage `json:"config"` 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 { 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"`
Selector string `json:"selector"`
Verifier []byte `json:"verifier"`
ExpiresAt time.Time `json:"expires_at"`
}
type User struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Login sql.NullString `json:"login"`
Passphrase []byte `json:"passphrase"`
TotpSecret sql.NullString `json:"totp_secret"`
IsAdmin bool `json:"is_admin"` IsAdmin bool `json:"is_admin"`
Password []byte `json:"password"` IsDisabled bool `json:"is_disabled"`
CreatedAt time.Time `json:"created_at"` 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"`
}

View file

@ -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)

View file

@ -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
} }

View file

@ -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;

View file

@ -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;

View 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;

View 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;

View file

@ -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;

View file

@ -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;

View file

@ -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.
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, "id" bigserial NOT NULL,
"login" citext NULL,
"passphrase" bytea NULL,
"totp_secret" text NULL,
"is_admin" boolean NOT NULL DEFAULT false, "is_admin" boolean NOT NULL DEFAULT false,
"password" bytea NULL, "is_disabled" boolean NOT NULL DEFAULT false,
"created_at" timestamptz NOT NULL DEFAULT NOW(), "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" (
"identity_id" bigserial 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 ("identity_id")
PRIMARY KEY ("selector") 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,
"type" text NOT NULL, "oidc_url" text NULL,
"config" jsonb NOT NULL, "auth_url" text NOT NULL,
PRIMARY KEY ("id") "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" ( 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");

View file

@ -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
}

View file

@ -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)
}