diff --git a/@app/client/src/graphql/AddEmail.graphql b/@app/client/src/graphql/AddEmail.graphql index f38d94cb..cc9b5989 100644 --- a/@app/client/src/graphql/AddEmail.graphql +++ b/@app/client/src/graphql/AddEmail.graphql @@ -1,6 +1,6 @@ #import "./EmailsForm_UserEmail.graphql" -mutation AddEmail($email: String!) { +mutation AddEmail($email: Email!) { createUserEmail(input: { userEmail: { email: $email } }) { user { id diff --git a/@app/client/src/graphql/ForgotPassword.graphql b/@app/client/src/graphql/ForgotPassword.graphql index 98865b2f..601766eb 100644 --- a/@app/client/src/graphql/ForgotPassword.graphql +++ b/@app/client/src/graphql/ForgotPassword.graphql @@ -1,4 +1,4 @@ -mutation ForgotPassword($email: String!) { +mutation ForgotPassword($email: Email!) { forgotPassword(input: { email: $email }) { # This mutation does not return any meaningful result, # but we still need to request _something_... diff --git a/@app/client/src/graphql/Register.graphql b/@app/client/src/graphql/Register.graphql index 39e95ff5..7aaa1aa2 100644 --- a/@app/client/src/graphql/Register.graphql +++ b/@app/client/src/graphql/Register.graphql @@ -1,7 +1,7 @@ mutation Register( $username: String! $password: String! - $email: String! + $email: Email! $name: String ) { register( diff --git a/@app/db/migrations/committed/000001.sql b/@app/db/migrations/committed/000001.sql index edbbb7a4..668eaff8 100644 --- a/@app/db/migrations/committed/000001.sql +++ b/@app/db/migrations/committed/000001.sql @@ -1,5 +1,5 @@ --! Previous: - ---! Hash: sha1:4be49e527161e4b03af6630d795e00271405d754 +--! Hash: sha1:269a8e30333545dd7d76a2591aa8637a27603ebb drop schema if exists app_public cascade; @@ -34,6 +34,11 @@ create schema app_private; /**********/ +create domain app_public."URL" as text check(VALUE ~ '^https?://[^/]+'); +create domain app_public.email as citext check(VALUE ~ '[^@]+@[^@]+\.[^@]+'); + +/**********/ + create function app_private.tg__add_job() returns trigger as $$ begin perform graphile_worker.add_job(tg_argv[0], json_build_object('id', NEW.id), coalesce(tg_argv[1], public.gen_random_uuid()::text)); @@ -121,7 +126,7 @@ create table app_public.users ( id serial primary key, username citext not null unique check(length(username) >= 2 and length(username) <= 24 and username ~ '^[a-zA-Z]([a-zA-Z0-9][_]?)+$'), name text, - avatar_url text check(avatar_url ~ '^https?://[^/]+'), + avatar_url app_public."URL", is_admin boolean not null default false, is_verified boolean not null default false, created_at timestamptz not null default now(), @@ -220,7 +225,7 @@ $$ language sql stable security definer set search_path to pg_catalog, public, p create table app_public.user_emails ( id serial primary key, user_id int not null default app_public.current_user_id() references app_public.users on delete cascade, - email citext not null check (email ~ '[^@]+@[^@]+\.[^@]+'), + email app_public.email not null, is_verified boolean not null default false, is_primary boolean not null default false, created_at timestamptz not null default now(), @@ -457,7 +462,7 @@ $$ language plpgsql security definer volatile set search_path to pg_catalog, pub /**********/ create table app_private.unregistered_email_password_resets ( - email citext constraint unregistered_email_pkey primary key, + email app_public.email constraint unregistered_email_pkey primary key, attempts int not null default 1, latest_attempt timestamptz not null ); @@ -470,7 +475,7 @@ comment on column app_private.unregistered_email_password_resets.latest_attempt /**********/ -create function app_public.forgot_password(email citext) returns void as $$ +create function app_public.forgot_password(email app_public.email) returns void as $$ declare v_user_email app_public.user_emails; v_token text; @@ -558,7 +563,7 @@ begin end; $$ language plpgsql strict security definer volatile set search_path to pg_catalog, public, pg_temp; -comment on function app_public.forgot_password(email public.citext) is +comment on function app_public.forgot_password(email app_public.email) is E'If you''ve forgotten your password, give us one of your email addresses and we''ll send you a reset token. Note this only works if you have added an email address!'; /**********/ @@ -747,10 +752,10 @@ grant execute on function app_public.change_password(text, text) to :DATABASE_VI create function app_private.really_create_user( username citext, - email text, + email app_public.email, email_is_verified bool, name text, - avatar_url text, + avatar_url app_public."URL", password text default null ) returns app_public.users as $$ declare @@ -787,7 +792,7 @@ begin end; $$ language plpgsql volatile set search_path to pg_catalog, public, pg_temp; -comment on function app_private.really_create_user(username citext, email text, email_is_verified bool, name text, avatar_url text, password text) is +comment on function app_private.really_create_user(username citext, email app_public.email, email_is_verified bool, name text, avatar_url app_public."URL", password text) is E'Creates a user account. All arguments are optional, it trusts the calling method to perform sanitisation.'; /**********/ @@ -801,10 +806,10 @@ create function app_private.register_user( ) returns app_public.users as $$ declare v_user app_public.users; - v_email citext; + v_email app_public.email; v_name text; v_username citext; - v_avatar_url text; + v_avatar_url app_public."URL"; v_user_authentication_id int; begin -- Extract data from the user’s OAuth profile data. @@ -874,9 +879,9 @@ create function app_private.link_or_register_user( declare v_matched_user_id int; v_matched_authentication_id int; - v_email citext; + v_email app_public.email; v_name text; - v_avatar_url text; + v_avatar_url app_public."URL"; v_user app_public.users; v_user_email app_public.user_emails; begin diff --git a/@app/graphql/codegen.yml b/@app/graphql/codegen.yml index ef17e15f..f4552f9b 100644 --- a/@app/graphql/codegen.yml +++ b/@app/graphql/codegen.yml @@ -4,6 +4,8 @@ documents: "../client/src/**/*.graphql" config: scalars: Datetime: "string" + Email: "string" + URL: "string" JSON: "{ [key: string]: any }" noGraphQLTag: false withHOC: false @@ -12,7 +14,9 @@ config: generates: index.tsx: plugins: - - add: "/* DO NOT EDIT! This file is auto-generated by graphql-code-generator - see `codegen.yml` */" + - add: + "/* DO NOT EDIT! This file is auto-generated by graphql-code-generator + - see `codegen.yml` */" - "typescript" - "typescript-operations" - "typescript-react-apollo" diff --git a/@app/server/src/middleware/installPostGraphile.ts b/@app/server/src/middleware/installPostGraphile.ts index fc5247cb..416c5010 100644 --- a/@app/server/src/middleware/installPostGraphile.ts +++ b/@app/server/src/middleware/installPostGraphile.ts @@ -12,6 +12,8 @@ import { Express, Request, Response } from "express"; import PgPubsub from "@graphile/pg-pubsub"; import PgSimplifyInflectorPlugin from "@graphile-contrib/pg-simplify-inflector"; import GraphilePro from "@graphile/pro"; // Requires license key +import PgTypeEmailPlugin from "../plugins/PgTypeEmailPlugin"; +import PgTypeUrlPlugin from "../plugins/PgTypeUrlPlugin"; import PassportLoginPlugin from "../plugins/PassportLoginPlugin"; import PrimaryKeyMutationsOnlyPlugin from "../plugins/PrimaryKeyMutationsOnlyPlugin"; import SubscriptionsPlugin from "../plugins/SubscriptionsPlugin"; @@ -151,6 +153,10 @@ export function getPostGraphileOptions({ * https://www.graphile.org/postgraphile/extending/ */ appendPlugins: [ + // Exposes `Email` and `URL` types in the GraphQL schema + PgTypeEmailPlugin, + PgTypeUrlPlugin, + // Adds support for our `postgraphile.tags.json5` file TagsFilePlugin, diff --git a/@app/server/src/plugins/PassportLoginPlugin.ts b/@app/server/src/plugins/PassportLoginPlugin.ts index dec8b05e..0c9f06ed 100644 --- a/@app/server/src/plugins/PassportLoginPlugin.ts +++ b/@app/server/src/plugins/PassportLoginPlugin.ts @@ -5,10 +5,10 @@ const PassportLoginPlugin = makeExtendSchemaPlugin(build => ({ typeDefs: gql` input RegisterInput { username: String! - email: String! + email: Email! password: String! name: String - avatarUrl: String + avatarUrl: URL } type RegisterPayload { diff --git a/@app/server/src/plugins/PgTypeEmailPlugin.ts b/@app/server/src/plugins/PgTypeEmailPlugin.ts new file mode 100644 index 00000000..197b21d2 --- /dev/null +++ b/@app/server/src/plugins/PgTypeEmailPlugin.ts @@ -0,0 +1,149 @@ +import { Plugin, Build, ScopeGraphQLScalarType } from "graphile-build"; +import { PgType } from "graphile-build-pg"; + +declare module "graphile-build" { + interface ScopeGraphQLScalarType { + isEmailScalar?: boolean; + } +} + +function isValidEmail(email: string) { + return /[^@]+@[^@]+\.[^@]+/.test(email); +} + +export default (function PgTypeEmailPlugin(builder) { + builder.hook( + "build", + build => { + // This hook tells graphile-build-pg about the email database type so it + // knows how to express it in input/output. + const { + pgIntrospectionResultsByKind: rawIntrospectionResultsByKind, + pgRegisterGqlTypeByTypeId, + pgRegisterGqlInputTypeByTypeId, + pg2GqlMapper, + pgSql: sql, + } = build; + + if ( + !rawIntrospectionResultsByKind || + !sql || + !pgRegisterGqlTypeByTypeId || + !pgRegisterGqlInputTypeByTypeId || + !pg2GqlMapper + ) { + throw new Error("Required helpers were not found on Build."); + } + + const introspectionResultsByKind = rawIntrospectionResultsByKind; + + // Get the 'email' type: + const emailType = introspectionResultsByKind.type.find( + (t: PgType) => t.name === "email" + ); + + if (!emailType) { + return build; + } + + const emailTypeName = build.inflection.builtin("Email"); + + const GraphQLEmailType = makeGraphQLEmailType( + build as Build, + emailTypeName + ); + + // Now register the Email type with the type system for both output and input. + pgRegisterGqlTypeByTypeId(emailType.id, () => GraphQLEmailType); + pgRegisterGqlInputTypeByTypeId(emailType.id, () => GraphQLEmailType); + + // Finally we must tell the system how to translate the data between PG-land and JS-land: + pg2GqlMapper[emailType.id] = { + // Turn string (from node-postgres) into email: no-op + map: (email: string) => email, + // When unmapping we need to convert back to SQL framgent + unmap: (email: string) => + sql.fragment`(${sql.value(email)}::${sql.identifier( + emailType.namespaceName, + emailType.name + )})`, + }; + + return build; + }, + ["PgTypeEmail"], + [], + ["PgTypes"] + ); + + /* End of email type */ +} as Plugin); + +function makeGraphQLEmailType(build: Build, emailTypeName: string) { + const { + graphql: { GraphQLScalarType, Kind }, + } = build; + function parseValue(obj: unknown): string { + if (!(typeof obj === "string")) { + throw new TypeError( + `This is not a valid ${emailTypeName} object, it must be a string.` + ); + } + if (!isValidEmail(obj)) { + throw new TypeError( + `This is not a properly formatted ${emailTypeName} object.` + ); + } + return obj; + } + + const parseLiteral: import("graphql").GraphQLScalarLiteralParser = ( + ast, + variables + ) => { + switch (ast.kind) { + case Kind.STRING: { + const email = ast.value; + if (!isValidEmail(email)) { + throw new TypeError( + `This is not a properly formatted ${emailTypeName} object.` + ); + } + return email; + } + + case Kind.NULL: + return null; + + case Kind.VARIABLE: { + const name = ast.name.value; + const email = variables ? variables[name] : undefined; + if (!isValidEmail(email)) { + throw new TypeError( + `This is not a properly formatted ${emailTypeName} object.` + ); + } + return email; + } + + default: + return undefined; + } + }; + + const scope: ScopeGraphQLScalarType = { isEmailScalar: true }; + const GraphQLEmailType = build.newWithHooks( + GraphQLScalarType, + { + name: emailTypeName, + description: + "An address in the electronic mail system. Email addresses such as `John.Smith@example.com` are made up of a local-part, followed by an `@` symbol, followed by a domain.", + serialize: (email: string) => email, + parseValue, + parseLiteral, + }, + scope + ); + + return GraphQLEmailType; +} diff --git a/@app/server/src/plugins/PgTypeUrlPlugin.ts b/@app/server/src/plugins/PgTypeUrlPlugin.ts new file mode 100644 index 00000000..4e200370 --- /dev/null +++ b/@app/server/src/plugins/PgTypeUrlPlugin.ts @@ -0,0 +1,127 @@ +import { Plugin, Build, ScopeGraphQLScalarType } from "graphile-build"; +import { PgType } from "graphile-build-pg"; +import { parse as parseUrl, format as formatUrl, Url } from "url"; + +declare module "graphile-build" { + interface ScopeGraphQLScalarType { + isUrlScalar?: boolean; + } +} + +export default (function PgTypeUrlPlugin(builder) { + builder.hook( + "build", + build => { + // This hook tells graphile-build-pg about the URL database type so it + // knows how to express it in input/output. + const { + pgIntrospectionResultsByKind: rawIntrospectionResultsByKind, + pgRegisterGqlTypeByTypeId, + pgRegisterGqlInputTypeByTypeId, + pg2GqlMapper, + pgSql: sql, + } = build; + + if ( + !rawIntrospectionResultsByKind || + !sql || + !pgRegisterGqlTypeByTypeId || + !pgRegisterGqlInputTypeByTypeId || + !pg2GqlMapper + ) { + throw new Error("Required helpers were not found on Build."); + } + + const introspectionResultsByKind = rawIntrospectionResultsByKind; + + // Get the 'URL' type: + const urlType = introspectionResultsByKind.type.find( + (t: PgType) => t.name === "URL" + ); + + if (!urlType) { + return build; + } + + const urlTypeName = build.inflection.builtin("URL"); + + const GraphQLURLType = makeGraphQLURLType(build as Build, urlTypeName); + + // Now register the URL type with the type system for both output and input. + pgRegisterGqlTypeByTypeId(urlType.id, () => GraphQLURLType); + pgRegisterGqlInputTypeByTypeId(urlType.id, () => GraphQLURLType); + + // Finally we must tell the system how to translate the data between PG-land and JS-land: + pg2GqlMapper[urlType.id] = { + // Turn string (from node-postgres) into URL object + map: parseUrl, + // When unmapping we need to convert back to string + unmap: (url: Url) => + sql.fragment`(${sql.value(formatUrl(url))}::${sql.identifier( + urlType.namespaceName, + urlType.name + )})`, + }; + + return build; + }, + ["PgTypeUrl"], + [], + ["PgTypes"] + ); + + /* End of URL type */ +} as Plugin); + +function makeGraphQLURLType(build: Build, urlTypeName: string) { + const { + graphql: { GraphQLScalarType, Kind }, + } = build; + function parseValue(obj: unknown): Url { + if (!(typeof obj === "string")) { + throw new TypeError( + `This is not a valid ${urlTypeName} object, it must be a string.` + ); + } + return parseUrl(obj); + } + + const parseLiteral: import("graphql").GraphQLScalarLiteralParser = ( + ast, + variables + ) => { + switch (ast.kind) { + case Kind.STRING: { + return parseUrl(ast.value); + } + + case Kind.NULL: + return null; + + case Kind.VARIABLE: { + const name = ast.name.value; + const value = variables ? variables[name] : undefined; + return parseUrl(value); + } + + default: + return undefined; + } + }; + + const scope: ScopeGraphQLScalarType = { isUrlScalar: true }; + const GraphQLURLType = build.newWithHooks( + GraphQLScalarType, + { + name: urlTypeName, + description: + "A Uniform Resource Locator (URL), colloquially termed a web address. It is a reference to a web resource that specifies its location on a computer network and a mechanism for retrieving it.", + serialize: (url: Url) => formatUrl(url), + parseValue, + parseLiteral, + }, + scope + ); + + return GraphQLURLType; +} diff --git a/data/schema.graphql b/data/schema.graphql index d50c8453..085fc255 100644 --- a/data/schema.graphql +++ b/data/schema.graphql @@ -164,6 +164,13 @@ type DeleteUserEmailPayload { ): UserEmailsEdge } +""" +An address in the electronic mail system. Email addresses such as +`John.Smith@example.com` are made up of a local-part, followed by an `@` symbol, +followed by a domain. +""" +scalar Email + """All input for the `forgotPassword` mutation.""" input ForgotPasswordInput { """ @@ -171,7 +178,7 @@ input ForgotPasswordInput { payload verbatim. May be used to track mutations by the client. """ clientMutationId: String - email: String! + email: Email! } """The output of our `forgotPassword` mutation.""" @@ -413,8 +420,8 @@ type Query { } input RegisterInput { - avatarUrl: String - email: String! + avatarUrl: URL + email: Email! name: String password: String! username: String! @@ -548,10 +555,17 @@ type UpdateUserPayload { ): UsersEdge } +""" +A Uniform Resource Locator (URL), colloquially termed a web address. It is a +reference to a web resource that specifies its location on a computer network +and a mechanism for retrieving it. +""" +scalar URL + """A user who can log in to the application.""" type User { """Optional avatar URL.""" - avatarUrl: String + avatarUrl: URL createdAt: Datetime! hasPassword: Boolean @@ -668,7 +682,7 @@ type UserEmail { createdAt: Datetime! """The users email address, in `a@b.c` format.""" - email: String! + email: Email! id: Int! isPrimary: Boolean! @@ -702,7 +716,7 @@ input UserEmailCondition { """An input for mutations affecting `UserEmail`""" input UserEmailInput { """The users email address, in `a@b.c` format.""" - email: String! + email: Email! } """A connection to a list of `UserEmail` values.""" @@ -747,7 +761,7 @@ enum UserEmailsOrderBy { """Represents an update to a `User`. Fields that are set will be updated.""" input UserPatch { """Optional avatar URL.""" - avatarUrl: String + avatarUrl: URL """Public-facing name (or pseudonym) of the user.""" name: String diff --git a/data/schema.sql b/data/schema.sql index b9eb40d2..a431ec44 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -2,8 +2,8 @@ -- PostgreSQL database dump -- --- Dumped from database version 11.6 (Debian 11.6-1.pgdg90+1) --- Dumped by pg_dump version 11.6 (Debian 11.6-1.pgdg90+1) +-- Dumped from database version 12.1 +-- Dumped by pg_dump version 12.1 SET statement_timeout = 0; SET lock_timeout = 0; @@ -79,6 +79,22 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public; COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)'; +-- +-- Name: URL; Type: DOMAIN; Schema: app_public; Owner: - +-- + +CREATE DOMAIN app_public."URL" AS text + CONSTRAINT "URL_check" CHECK ((VALUE ~ '^https?://[^/]+'::text)); + + +-- +-- Name: email; Type: DOMAIN; Schema: app_public; Owner: - +-- + +CREATE DOMAIN app_public.email AS public.citext + CONSTRAINT email_check CHECK ((VALUE OPERATOR(public.~) '[^@]+@[^@]+\.[^@]+'::public.citext)); + + -- -- Name: assert_valid_password(text); Type: FUNCTION; Schema: app_private; Owner: - -- @@ -97,7 +113,7 @@ $$; SET default_tablespace = ''; -SET default_with_oids = false; +SET default_table_access_method = heap; -- -- Name: users; Type: TABLE; Schema: app_public; Owner: - @@ -107,12 +123,11 @@ CREATE TABLE app_public.users ( id integer NOT NULL, username public.citext NOT NULL, name text, - avatar_url text, + avatar_url app_public."URL", is_admin boolean DEFAULT false NOT NULL, is_verified boolean DEFAULT false NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT users_avatar_url_check CHECK ((avatar_url ~ '^https?://[^/]+'::text)), CONSTRAINT users_username_check CHECK (((length((username)::text) >= 2) AND (length((username)::text) <= 24) AND (username OPERATOR(public.~) '^[a-zA-Z]([a-zA-Z0-9][_]?)+$'::public.citext))) ); @@ -170,9 +185,9 @@ CREATE FUNCTION app_private.link_or_register_user(f_user_id integer, f_service c declare v_matched_user_id int; v_matched_authentication_id int; - v_email citext; + v_email app_public.email; v_name text; - v_avatar_url text; + v_avatar_url app_public."URL"; v_user app_public.users; v_user_email app_public.user_emails; begin @@ -345,10 +360,10 @@ COMMENT ON FUNCTION app_private.login(username public.citext, password text) IS -- --- Name: really_create_user(public.citext, text, boolean, text, text, text); Type: FUNCTION; Schema: app_private; Owner: - +-- Name: really_create_user(public.citext, app_public.email, boolean, text, app_public."URL", text); Type: FUNCTION; Schema: app_private; Owner: - -- -CREATE FUNCTION app_private.really_create_user(username public.citext, email text, email_is_verified boolean, name text, avatar_url text, password text DEFAULT NULL::text) RETURNS app_public.users +CREATE FUNCTION app_private.really_create_user(username public.citext, email app_public.email, email_is_verified boolean, name text, avatar_url app_public."URL", password text DEFAULT NULL::text) RETURNS app_public.users LANGUAGE plpgsql SET search_path TO 'pg_catalog', 'public', 'pg_temp' AS $$ @@ -388,10 +403,10 @@ $$; -- --- Name: FUNCTION really_create_user(username public.citext, email text, email_is_verified boolean, name text, avatar_url text, password text); Type: COMMENT; Schema: app_private; Owner: - +-- Name: FUNCTION really_create_user(username public.citext, email app_public.email, email_is_verified boolean, name text, avatar_url app_public."URL", password text); Type: COMMENT; Schema: app_private; Owner: - -- -COMMENT ON FUNCTION app_private.really_create_user(username public.citext, email text, email_is_verified boolean, name text, avatar_url text, password text) IS 'Creates a user account. All arguments are optional, it trusts the calling method to perform sanitisation.'; +COMMENT ON FUNCTION app_private.really_create_user(username public.citext, email app_public.email, email_is_verified boolean, name text, avatar_url app_public."URL", password text) IS 'Creates a user account. All arguments are optional, it trusts the calling method to perform sanitisation.'; -- @@ -404,10 +419,10 @@ CREATE FUNCTION app_private.register_user(f_service character varying, f_identif AS $$ declare v_user app_public.users; - v_email citext; + v_email app_public.email; v_name text; v_username citext; - v_avatar_url text; + v_avatar_url app_public."URL"; v_user_authentication_id int; begin -- Extract data from the user’s OAuth profile data. @@ -725,10 +740,10 @@ COMMENT ON FUNCTION app_public.current_user_id() IS 'Handy method to get the cur -- --- Name: forgot_password(public.citext); Type: FUNCTION; Schema: app_public; Owner: - +-- Name: forgot_password(app_public.email); Type: FUNCTION; Schema: app_public; Owner: - -- -CREATE FUNCTION app_public.forgot_password(email public.citext) RETURNS void +CREATE FUNCTION app_public.forgot_password(email app_public.email) RETURNS void LANGUAGE plpgsql STRICT SECURITY DEFINER SET search_path TO 'pg_catalog', 'public', 'pg_temp' AS $$ @@ -821,10 +836,10 @@ $$; -- --- Name: FUNCTION forgot_password(email public.citext); Type: COMMENT; Schema: app_public; Owner: - +-- Name: FUNCTION forgot_password(email app_public.email); Type: COMMENT; Schema: app_public; Owner: - -- -COMMENT ON FUNCTION app_public.forgot_password(email public.citext) IS 'If you''ve forgotten your password, give us one of your email addresses and we''ll send you a reset token. Note this only works if you have added an email address!'; +COMMENT ON FUNCTION app_public.forgot_password(email app_public.email) IS 'If you''ve forgotten your password, give us one of your email addresses and we''ll send you a reset token. Note this only works if you have added an email address!'; -- @@ -851,12 +866,11 @@ $$; CREATE TABLE app_public.user_emails ( id integer NOT NULL, user_id integer DEFAULT app_public.current_user_id() NOT NULL, - email public.citext NOT NULL, + email app_public.email NOT NULL, is_verified boolean DEFAULT false NOT NULL, is_primary boolean DEFAULT false NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT user_emails_email_check CHECK ((email OPERATOR(public.~) '[^@]+@[^@]+\.[^@]+'::public.citext)), CONSTRAINT user_emails_must_be_verified_to_be_primary CHECK (((is_primary IS FALSE) OR (is_verified IS TRUE))) ); @@ -1225,7 +1239,7 @@ CREATE TABLE app_private.connect_pg_simple_sessions ( -- CREATE TABLE app_private.unregistered_email_password_resets ( - email public.citext NOT NULL, + email app_public.email NOT NULL, attempts integer DEFAULT 1 NOT NULL, latest_attempt timestamp with time zone NOT NULL ); @@ -1573,70 +1587,70 @@ CREATE INDEX user_authentications_user_id_idx ON app_public.user_authentications -- Name: user_authentications _100_timestamps; Type: TRIGGER; Schema: app_public; Owner: - -- -CREATE TRIGGER _100_timestamps BEFORE INSERT OR UPDATE ON app_public.user_authentications FOR EACH ROW EXECUTE PROCEDURE app_private.tg__timestamps(); +CREATE TRIGGER _100_timestamps BEFORE INSERT OR UPDATE ON app_public.user_authentications FOR EACH ROW EXECUTE FUNCTION app_private.tg__timestamps(); -- -- Name: user_emails _100_timestamps; Type: TRIGGER; Schema: app_public; Owner: - -- -CREATE TRIGGER _100_timestamps BEFORE INSERT OR UPDATE ON app_public.user_emails FOR EACH ROW EXECUTE PROCEDURE app_private.tg__timestamps(); +CREATE TRIGGER _100_timestamps BEFORE INSERT OR UPDATE ON app_public.user_emails FOR EACH ROW EXECUTE FUNCTION app_private.tg__timestamps(); -- -- Name: users _100_timestamps; Type: TRIGGER; Schema: app_public; Owner: - -- -CREATE TRIGGER _100_timestamps BEFORE INSERT OR UPDATE ON app_public.users FOR EACH ROW EXECUTE PROCEDURE app_private.tg__timestamps(); +CREATE TRIGGER _100_timestamps BEFORE INSERT OR UPDATE ON app_public.users FOR EACH ROW EXECUTE FUNCTION app_private.tg__timestamps(); -- -- Name: user_emails _200_forbid_existing_email; Type: TRIGGER; Schema: app_public; Owner: - -- -CREATE TRIGGER _200_forbid_existing_email BEFORE INSERT ON app_public.user_emails FOR EACH ROW EXECUTE PROCEDURE app_public.tg_user_emails__forbid_if_verified(); +CREATE TRIGGER _200_forbid_existing_email BEFORE INSERT ON app_public.user_emails FOR EACH ROW EXECUTE FUNCTION app_public.tg_user_emails__forbid_if_verified(); -- -- Name: users _200_make_first_user_admin; Type: TRIGGER; Schema: app_public; Owner: - -- -CREATE TRIGGER _200_make_first_user_admin BEFORE INSERT ON app_public.users FOR EACH ROW WHEN ((new.id = 1)) EXECUTE PROCEDURE app_private.tg_users__make_first_user_admin(); +CREATE TRIGGER _200_make_first_user_admin BEFORE INSERT ON app_public.users FOR EACH ROW WHEN ((new.id = 1)) EXECUTE FUNCTION app_private.tg_users__make_first_user_admin(); -- -- Name: users _500_gql_update; Type: TRIGGER; Schema: app_public; Owner: - -- -CREATE TRIGGER _500_gql_update AFTER UPDATE ON app_public.users FOR EACH ROW EXECUTE PROCEDURE app_public.tg__graphql_subscription('userChanged', 'graphql:user:$1', 'id'); +CREATE TRIGGER _500_gql_update AFTER UPDATE ON app_public.users FOR EACH ROW EXECUTE FUNCTION app_public.tg__graphql_subscription('userChanged', 'graphql:user:$1', 'id'); -- -- Name: user_emails _500_insert_secrets; Type: TRIGGER; Schema: app_public; Owner: - -- -CREATE TRIGGER _500_insert_secrets AFTER INSERT ON app_public.user_emails FOR EACH ROW EXECUTE PROCEDURE app_private.tg_user_email_secrets__insert_with_user_email(); +CREATE TRIGGER _500_insert_secrets AFTER INSERT ON app_public.user_emails FOR EACH ROW EXECUTE FUNCTION app_private.tg_user_email_secrets__insert_with_user_email(); -- -- Name: users _500_insert_secrets; Type: TRIGGER; Schema: app_public; Owner: - -- -CREATE TRIGGER _500_insert_secrets AFTER INSERT ON app_public.users FOR EACH ROW EXECUTE PROCEDURE app_private.tg_user_secrets__insert_with_user(); +CREATE TRIGGER _500_insert_secrets AFTER INSERT ON app_public.users FOR EACH ROW EXECUTE FUNCTION app_private.tg_user_secrets__insert_with_user(); -- -- Name: user_emails _500_verify_account_on_verified; Type: TRIGGER; Schema: app_public; Owner: - -- -CREATE TRIGGER _500_verify_account_on_verified AFTER INSERT OR UPDATE OF is_verified ON app_public.user_emails FOR EACH ROW WHEN ((new.is_verified IS TRUE)) EXECUTE PROCEDURE app_public.tg_user_emails__verify_account_on_verified(); +CREATE TRIGGER _500_verify_account_on_verified AFTER INSERT OR UPDATE OF is_verified ON app_public.user_emails FOR EACH ROW WHEN ((new.is_verified IS TRUE)) EXECUTE FUNCTION app_public.tg_user_emails__verify_account_on_verified(); -- -- Name: user_emails _900_send_verification_email; Type: TRIGGER; Schema: app_public; Owner: - -- -CREATE TRIGGER _900_send_verification_email AFTER INSERT ON app_public.user_emails FOR EACH ROW WHEN ((new.is_verified IS FALSE)) EXECUTE PROCEDURE app_private.tg__add_job('user_emails__send_verification'); +CREATE TRIGGER _900_send_verification_email AFTER INSERT ON app_public.user_emails FOR EACH ROW WHEN ((new.is_verified IS FALSE)) EXECUTE FUNCTION app_private.tg__add_job('user_emails__send_verification'); -- @@ -1857,10 +1871,10 @@ REVOKE ALL ON FUNCTION app_private.login(username public.citext, password text) -- --- Name: FUNCTION really_create_user(username public.citext, email text, email_is_verified boolean, name text, avatar_url text, password text); Type: ACL; Schema: app_private; Owner: - +-- Name: FUNCTION really_create_user(username public.citext, email app_public.email, email_is_verified boolean, name text, avatar_url app_public."URL", password text); Type: ACL; Schema: app_private; Owner: - -- -REVOKE ALL ON FUNCTION app_private.really_create_user(username public.citext, email text, email_is_verified boolean, name text, avatar_url text, password text) FROM PUBLIC; +REVOKE ALL ON FUNCTION app_private.really_create_user(username public.citext, email app_public.email, email_is_verified boolean, name text, avatar_url app_public."URL", password text) FROM PUBLIC; -- @@ -1946,11 +1960,11 @@ GRANT ALL ON FUNCTION app_public.current_user_id() TO graphile_starter_visitor; -- --- Name: FUNCTION forgot_password(email public.citext); Type: ACL; Schema: app_public; Owner: - +-- Name: FUNCTION forgot_password(email app_public.email); Type: ACL; Schema: app_public; Owner: - -- -REVOKE ALL ON FUNCTION app_public.forgot_password(email public.citext) FROM PUBLIC; -GRANT ALL ON FUNCTION app_public.forgot_password(email public.citext) TO graphile_starter_visitor; +REVOKE ALL ON FUNCTION app_public.forgot_password(email app_public.email) FROM PUBLIC; +GRANT ALL ON FUNCTION app_public.forgot_password(email app_public.email) TO graphile_starter_visitor; --