Last active
January 15, 2018 23:55
-
-
Save parris/3f0b23796b7d8490613c0cef6dfd8c48 to your computer and use it in GitHub Desktop.
Fervor + PostgraphQL/Postgraphile + Knex Authentication Example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
exports.up = (db, Promise) => ( | |
Promise.all([ | |
db.schema.raw('ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM public;'), | |
db.schema.raw("CREATE TYPE public.user_role AS ENUM ('hatch_anonymous', 'hatch_contributor', 'hatch_admin')"), | |
db.schema.raw('CREATE ROLE hatch_anonymous;'), | |
db.schema.raw('CREATE ROLE hatch_contributor;'), | |
db.schema.raw('CREATE ROLE hatch_admin;'), | |
db.schema.raw('GRANT USAGE ON SCHEMA public to hatch_anonymous, hatch_contributor, hatch_admin;'), | |
db.schema.raw('CREATE EXTENSION IF NOT EXISTS "pgcrypto";'), | |
db.schema.createTable('public.person', (table) => { | |
table.increments(); | |
table.string('email').notNull().defaultTo('').unique(); | |
table.string('password_hash').notNull().defaultTo(''); | |
table.string('password_salt').notNull().defaultTo(''); | |
table.specificType('role', 'user_role').notNull().defaultTo('hatch_contributor'); | |
table.boolean('verified').notNull().defaultTo(false); | |
table.string('display_name').nullable(); | |
table.timestamps(true, true); | |
}), | |
db.schema.raw(` | |
CREATE TYPE public.jwt_token AS ( | |
role text, | |
person_id integer | |
); | |
`), | |
]).then((() => (Promise.all([ | |
db.schema.raw(` | |
CREATE FUNCTION public.register_person( | |
display_name TEXT, | |
email TEXT, | |
password TEXT | |
) RETURNS public.person AS $$ | |
DECLARE | |
p public.person; | |
salt TEXT; | |
passhash TEXT; | |
BEGIN | |
salt := gen_salt('bf'); | |
passhash := crypt(password, salt); | |
INSERT INTO public.person (display_name, email, password_hash, password_salt) VALUES | |
(display_name, email, passhash, salt) | |
RETURNING * INTO p; | |
RETURN p; | |
END; | |
$$ LANGUAGE plpgsql STRICT SECURITY DEFINER; | |
comment on function register_person(TEXT, TEXT, TEXT) is 'Registers a single user.'; | |
`), | |
db.schema.raw(` | |
CREATE FUNCTION public.authenticate( | |
email TEXT, | |
password TEXT | |
) RETURNS public.jwt_token AS $$ | |
DECLARE | |
account public.person; | |
BEGIN | |
SELECT p.* | |
INTO account | |
FROM public.person AS p | |
WHERE p.email = $1; | |
IF account.password_hash = crypt(password, account.password_salt) | |
THEN | |
RETURN (account.role, account.id) :: public.jwt_token; | |
ELSE | |
RETURN NULL; | |
END IF; | |
END; | |
$$ LANGUAGE plpgsql STRICT SECURITY DEFINER; | |
comment on function public.authenticate(TEXT, TEXT) is 'Authenticates a user and returns a JWT'; | |
`), | |
db.schema.raw(` | |
CREATE FUNCTION public.current_person() RETURNS public.person AS $$ | |
SELECT * FROM public.person | |
WHERE id = current_setting('jwt.claims.person_id')::integer | |
$$ language sql stable; | |
comment on function public.current_person() is 'Gets the person who was identified by our JWT.'; | |
`), | |
db.schema.raw('GRANT SELECT ON TABLE public.person TO hatch_anonymous, hatch_contributor, hatch_admin;'), | |
db.schema.raw('GRANT SELECT, UPDATE, INSERT, DELETE ON TABLE public.person TO hatch_admin;'), | |
db.schema.raw('GRANT USAGE ON SEQUENCE public.person_id_seq TO hatch_anonymous, hatch_contributor, hatch_admin'), | |
db.schema.raw('GRANT EXECUTE ON FUNCTION public.authenticate(TEXT, TEXT) TO hatch_anonymous, hatch_contributor, hatch_admin'), | |
db.schema.raw('GRANT EXECUTE ON FUNCTION public.current_person() TO hatch_contributor, hatch_admin'), | |
db.schema.raw('GRANT EXECUTE ON FUNCTION public.register_person(TEXT, TEXT, TEXT) TO hatch_anonymous, hatch_contributor, hatch_admin'), | |
db.schema.raw('CREATE POLICY select_person ON public.person FOR SELECT TO hatch_anonymous, hatch_contributor, hatch_admin USING (true);'), | |
db.schema.raw('ALTER TABLE public.person ENABLE ROW LEVEL SECURITY;'), | |
db.schema.raw('CREATE POLICY insert_person ON public.person FOR INSERT WITH CHECK (true);'), | |
db.schema.raw("CREATE POLICY update_person ON public.person FOR UPDATE USING (id = current_setting('jwt.claims.person_id')::integer) WITH CHECK (id = current_setting('jwt.claims.person_id')::integer);"), | |
db.schema.raw('CREATE POLICY delete_person ON public.person FOR DELETE TO hatch_admin;'), | |
])))) | |
); | |
exports.down = (db, Promise) => ( | |
Promise.all([ | |
db.schema.raw('DROP FUNCTION IF EXISTS public.register_person(TEXT, TEXT, TEXT)'), | |
db.schema.raw('DROP FUNCTION IF EXISTS public.authenticate(TEXT, TEXT)'), | |
db.schema.raw('DROP FUNCTION IF EXISTS public.current_person()'), | |
db.schema.dropTableIfExists('public.person'), | |
db.schema.raw('DROP TYPE IF EXISTS public.jwt_token'), | |
db.schema.raw('DROP TYPE IF EXISTS public.user_role'), | |
]).then(() => Promise.all([ | |
db.schema.raw('REVOKE ALL PRIVILEGES ON SCHEMA public FROM hatch_anonymous;'), | |
db.schema.raw('REVOKE ALL PRIVILEGES ON SCHEMA public FROM hatch_contributor;'), | |
db.schema.raw('REVOKE ALL PRIVILEGES ON SCHEMA public FROM hatch_admin;'), | |
])).then(() => Promise.all([ | |
db.schema.raw('DROP ROLE hatch_anonymous;'), | |
db.schema.raw('DROP ROLE hatch_contributor;'), | |
db.schema.raw('DROP ROLE hatch_admin;'), | |
])) | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/graph/index.js | |
function removePrivatePersonFields(builder) { | |
builder.hook('GraphQLObjectType:fields', (fields, _, { GraphQLObjectType }) => { | |
if (GraphQLObjectType.name === 'Person') { | |
delete fields.email; | |
delete fields.passwordHash; | |
delete fields.passwordSalt; | |
delete fields.verified; | |
} | |
return fields; | |
}); | |
builder.hook('GraphQLInputObjectType:fields', (fields, _, { GraphQLInputObjectType }) => { | |
if (GraphQLInputObjectType.name === 'PersonPatch') { | |
delete fields.email; | |
delete fields.passwordHash; | |
delete fields.passwordSalt; | |
delete fields.verified; | |
} | |
return fields; | |
}); | |
} | |
function removePrivateQueries(builder) { | |
builder.hook('GraphQLObjectType:fields', (fields, _, { scope: { isRootQuery } }) => { | |
if (!isRootQuery) { return fields; } | |
delete fields.allKnexMigrations; | |
delete fields.allKnexMigrationsLocks; | |
delete fields.knexMigrationById; | |
delete fields.knexMigration; | |
delete fields.allPeople; | |
delete fields.crypt; | |
delete fields.dearmor; | |
delete fields.decrypt; | |
delete fields.decryptIv; | |
delete fields.encrypt; | |
delete fields.encryptIv; | |
delete fields.pgpKeyId; | |
return fields; | |
}); | |
} | |
function removePrivateMutations(builder) { | |
builder.hook('GraphQLObjectType:fields', (spec) => { | |
delete spec.createKnexMigration; | |
delete spec.createKnexMigrationsLock; | |
delete spec.updateKnexMigration; | |
delete spec.updateKnexMigrationById; | |
delete spec.deleteKnexMigration; | |
delete spec.deleteKnexMigrationById; | |
delete spec.createPerson; | |
delete spec.deletePerson; | |
delete spec.deletePersonById; | |
delete spec.genRandomBytes; | |
delete spec.genRandomUuid; | |
return spec; | |
}); | |
} | |
export default () => ({ | |
graphiql: true, | |
graphiqlRoute: '/admin/graphiql', | |
jwtSecret: process.env.JWT_SECRET, | |
jwtPgTypeIdentifier: 'public.jwt_token', | |
appendPlugins: [ | |
removePrivatePersonFields, | |
removePrivateQueries, | |
removePrivateMutations, | |
], | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/apps/Login.js | |
import { React, Form, clientCookies, gql } from 'fervor/lib'; | |
import Template from '../components/Template'; | |
import styles from './styles/about.scss'; | |
const authenticate = gql` | |
mutation Authenticate($login: AuthenticateInput!) { | |
authenticate(input: $login) { | |
jwtToken | |
} | |
}`; | |
const onSuccess = (res) => { | |
clientCookies.set('authJWT', res.data.authenticate.jwtToken); | |
}; | |
export default () => ( | |
<Template title="Login"> | |
<article className={styles.article}> | |
<Form mutation={authenticate} onSuccess={onSuccess}> | |
<input name="login[email]" placeholder="Email" /> | |
<input name="login[password]" type="password" placeholder="password" /> | |
<button type="submit">Submit</button> | |
</Form> | |
</article> | |
</Template> | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/apps/Register.js | |
import { React, Form, gql } from 'fervor/lib'; | |
import Template from '../components/Template'; | |
import styles from './styles/about.scss'; | |
const authenticate = gql` | |
mutation Register($register: RegisterPersonInput!) { | |
registerPerson(input: $register) { | |
person { | |
id | |
role | |
} | |
} | |
}`; | |
export default () => ( | |
<Template title="hatch"> | |
<article className={styles.article}> | |
<Form mutation={authenticate} redirectTo="/login"> | |
<input name="register[givenName]" placeholder="First Name" /> | |
<input name="register[familyName]" placeholder="Last Name" /> | |
<input name="register[email]" placeholder="Email" /> | |
<input name="register[password]" type="password" placeholder="password" /> | |
<button type="submit">Submit</button> | |
</Form> | |
</article> | |
</Template> | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
knex migrate:latest
says it should be:(db, Promise) => ( return Promise.all([