Skip to content
This repository has been archived by the owner on May 30, 2021. It is now read-only.

Commit

Permalink
Merge pull request #11 from RodolfoSilva/organization
Browse files Browse the repository at this point in the history
Add organization support
  • Loading branch information
RodolfoSilva authored Sep 14, 2019
2 parents 02ba2ef + 07ea5a4 commit 8e8bb2e
Show file tree
Hide file tree
Showing 14 changed files with 150 additions and 41 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ JWT_ALGORITHM=HS256
JWT_PRIVATE_KEY=secretkey
JWT_TOKEN_EXPIRES=15
JWT_REFRESH_TOKEN_EXPIRES=1440
ALLOW_EMPTY_ORGANIZATION=true
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,29 @@ CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public;

CREATE TABLE "role" (
name text NOT NULL PRIMARY KEY,
created_at timestamp with time zone NOT NULL DEFAULT now(),
created_at timestamp with time zone NOT NULL DEFAULT now()
);

INSERT INTO "roles" ("name") VALUES ('user');
INSERT INTO "role" ("name") VALUES ('user');

CREATE TABLE "organization" (
id uuid DEFAULT gen_random_uuid() NOT NULL CONSTRAINT organization_pkey PRIMARY KEY,
name text NOT NULL,
created_at timestamp WITH TIME ZONE DEFAULT now() NOT NULL,
updated_at timestamp WITH TIME ZONE DEFAULT now() NOT NULL
);

CREATE TABLE "user" (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email citext NOT NULL UNIQUE,
organization_id uuid NULL CONSTRAINT user_organization_id_fkey REFERENCES "organization" ON UPDATE CASCADE ON DELETE CASCADE,
email citext NOT NULL,
password text NOT NULL,
default_role text NOT NULL DEFAULT 'user',
is_active boolean NOT NULL DEFAULT false,
secret_token uuid NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT user_email_organization_id_key UNIQUE (email, organization_id),
FOREIGN KEY (default_role) REFERENCES role (name)
);

Expand Down Expand Up @@ -88,7 +97,8 @@ In "Headers for the remote GraphQL server" select the option "Forward all header
| `JWT_ALGORITHM` | `HS256` | JWT Algorithm |
| `JWT_PRIVATE_KEY` | `secretkey` | JWT Secret key used to generate token |
| `JWT_TOKEN_EXPIRES` | `15` | Life time in minutes of JWT |
| `allowRegistrationFor` | `*` | Allow registration by role |
| `ALLOW_REGISTRATION_FOR` | `*` | Allow registration by role |
| `ALLOW_EMPTY_ORGANIZATION` | `true` | Allow use empty organization |

## Todo

Expand Down
2 changes: 2 additions & 0 deletions docker-compose.development.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ services:
# command: yarn start:dev
volumes:
- /app/node_modules
environment:
ALLOW_EMPTY_ORGANIZATION: 'false'
9 changes: 7 additions & 2 deletions src/auth-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@ export const generateJwtRefreshToken = (payload: any) => {
export const generateClaimsJwtToken = (user: User, sessionId: string) => {
const payload = {
[vars.hasuraGraphqlClaimsKey]: {
[`${vars.hasuraHeaderPrefix}allowed-roles`]: uniq([user.default_role, ...user.user_roles.map(({ role }) => role)]).filter(role => !!role),
[`${vars.hasuraHeaderPrefix}allowed-roles`]: uniq([
user.default_role,
...user.user_roles.map(({ role }) => role),
]).filter(role => !!role),
[`${vars.hasuraHeaderPrefix}default-role`]: user.default_role,
[`${vars.hasuraHeaderPrefix}user-id`]: user.id.toString(),
[`${vars.hasuraHeaderPrefix}session-id`]: sessionId,
[`${vars.hasuraHeaderPrefix}user-id`]: user.id.toString(),
[`${vars.hasuraHeaderPrefix}organization-id`]:
user.organization_id === null ? '' : user.organization_id,
},
};

Expand Down
2 changes: 2 additions & 0 deletions src/hasura/activate-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import uuidv4 from 'uuid/v4';
import { updateUser } from './update-user';

export const activateUser = async (
organizationId: string | null,
email: string,
secretToken: string,
): Promise<boolean> => {
const result = await updateUser(
{
email: { _eq: email },
organization_id: { _eq: organizationId },
secret_token: { _eq: secretToken },
is_active: { _eq: false },
},
Expand Down
9 changes: 7 additions & 2 deletions src/hasura/create-user-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import gql from 'graphql-tag';
import { hasuraQuery } from './client';
import { USER_FRAGMENT } from './user-fragment';
import { User } from './user-type';
import { getUserByEmail } from './get-user-by-email';
import { getUserByOrganizationIdAndEmail } from './get-user-by-organization-id-email';
import { userRegistrationAutoActive } from '../vars';

export const createUserAccount = async (
organizationId: string | null,
email: string,
password: string,
): Promise<User> => {
const user: User | undefined = await getUserByEmail(email);
const user: User | undefined = await getUserByOrganizationIdAndEmail(
organizationId,
email,
);

if (user) {
throw new Error('Email already registered');
Expand All @@ -36,6 +40,7 @@ export const createUserAccount = async (
email: email.toLowerCase(),
password: passwordHash,
secret_token: uuidv4(),
organization_id: organizationId,
is_active: userRegistrationAutoActive,
},
},
Expand Down
8 changes: 6 additions & 2 deletions src/hasura/get-user-by-credentials.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import bcrypt from 'bcryptjs';
import { User } from './user-type';
import { getUserByEmail } from './get-user-by-email';
import { getUserByOrganizationIdAndEmail } from './get-user-by-organization-id-email';

export const getUserByCredentials = async (
organizationId: string | null,
email: string,
password: string,
): Promise<User> => {
const user: User | undefined = await getUserByEmail(email);
const user: User | undefined = await getUserByOrganizationIdAndEmail(
organizationId,
email,
);

if (!user) {
throw new Error('Invalid "email" or "password"');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { hasuraQuery } from './client';
import { User } from './user-type';
import { USER_FRAGMENT } from './user-fragment';

export const getUserByEmail = async (
export const getUserByOrganizationIdAndEmail = async (
organizationId: string | null,
email: string,
): Promise<User | undefined> => {
try {
Expand All @@ -19,6 +20,7 @@ export const getUserByEmail = async (
`,
{
where: {
organization_id: { _eq: organizationId },
email: { _eq: email.toLowerCase() },
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/hasura/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { User } from './user-type';
export { getUserById } from './get-user-by-id';
export { getUserByEmail } from './get-user-by-email';
import { getUserByOrganizationIdAndEmail } from './get-user-by-organization-id-email';
export { getUserByCredentials } from './get-user-by-credentials';
export { createUserSession } from './create-user-session';
export { createUserAccount } from './create-user-account';
Expand Down
7 changes: 7 additions & 0 deletions src/hasura/user-fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import gql from 'graphql-tag';
export const USER_FRAGMENT = gql`
fragment UserParts on user {
id
organization_id
email
password
is_active
Expand All @@ -12,6 +13,12 @@ export const USER_FRAGMENT = gql`
id
role
}
organization {
id
name
created_at
updated_at
}
created_at
updated_at
}
Expand Down
1 change: 1 addition & 0 deletions src/hasura/user-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface Role {

export interface User {
id: string;
organization_id: string | null;
email: string;
is_active: boolean;
default_role: string;
Expand Down
76 changes: 56 additions & 20 deletions src/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ import {
getUserByCredentials,
} from './hasura';

const getRole = (req: Request) => getIn(req, `headers["${vars.hasuraHeaderPrefix}role"]`, '');
const getRole = (req: Request) =>
getIn(req, `headers["${vars.hasuraHeaderPrefix}role"]`, '');
const isAdmin = (req: Request) => getRole(req) === 'admin';

const getDataFromVerifiedAuthorizationToken = (req: Request): undefined | any => {
const getDataFromVerifiedAuthorizationToken = (
req: Request,
): undefined | any => {
const { authorization } = req.headers;

if (authorization === undefined) {
Expand All @@ -38,10 +41,14 @@ const getDataFromVerifiedAuthorizationToken = (req: Request): undefined | any =>
const getFieldFromDataAuthorizationToken = (req: Request, field) => {
const verifiedToken: any = getDataFromVerifiedAuthorizationToken(req);

return getIn(verifiedToken, `["${vars.hasuraGraphqlClaimsKey}"]${vars.hasuraHeaderPrefix}${field}`);
return getIn(
verifiedToken,
`["${vars.hasuraGraphqlClaimsKey}"]${vars.hasuraHeaderPrefix}${field}`,
);
};

const getCurrentUserId = (req: Request) => getFieldFromDataAuthorizationToken(req, 'user-id');
const getCurrentUserId = (req: Request) =>
getFieldFromDataAuthorizationToken(req, 'user-id');

const isAuthenticated = (req: Request): boolean => {
return !!getDataFromVerifiedAuthorizationToken(req);
Expand Down Expand Up @@ -100,13 +107,21 @@ const resolvers = {

return getUserById(currentUserId);
} catch (e) {
throw new Error('Not logged in.');
throw new Error('Not logged in');
}
},
},
Mutation: {
async auth_login(_, { email, password }, ctx) {
const user: User = await getUserByCredentials(email, password);
async auth_login(_, { organization_id, email, password }, ctx) {
if (!vars.allowEmptyOrganization && !organization_id) {
throw new Error('Missing organization_id');
}

const user: User = await getUserByCredentials(
organization_id,
email,
password,
);

const ipAddress = (
ctx.req.headers['x-forwarded-for'] ||
Expand All @@ -125,19 +140,27 @@ const resolvers = {
const accessToken = generateClaimsJwtToken(user, sessionId);

return {
accessToken,
refreshToken: generateJwtRefreshToken({
access_token: accessToken,
refresh_token: generateJwtRefreshToken({
token: refreshToken,
}),
userId: user.id,
organization_id: user.organization_id,
user_id: user.id,
};
},
async auth_register(_, { email, password }, ctx) {
async auth_register(_, { organization_id, email, password }, ctx) {
if (!vars.allowEmptyOrganization && !organization_id) {
throw new Error('Missing organization_id');
}

if (!checkUserCanDoRegistration(ctx.req)) {
throw new Error('Forbidden');
}
const user = await createUserAccount(email, password);
return user !== undefined;

const user = await createUserAccount(organization_id, email, password);
return {
affected_rows: Number(user !== undefined),
};
},
async auth_change_password(_, { user_id, new_password }, ctx) {
if (!checkUserIsPartOfStaffOrIsTheCurrentUser(ctx.req, user_id)) {
Expand All @@ -147,12 +170,20 @@ const resolvers = {
const user: User | undefined = await getUserById(user_id);

if (!user) {
throw new Error('Unable to find user.');
throw new Error('Unable to find user');
}

return await changeUserPassword(user, new_password);
const result = await changeUserPassword(user, new_password);

return {
affected_rows: Number(result),
};
},
async auth_activate_account(_, { email, secret_token }) {
async auth_activate_account(_, { organization_id, email, secret_token }) {
if (!vars.allowEmptyOrganization && !organization_id) {
throw new Error('Missing organization_id');
}

if (isEmpty(email)) {
throw new Error('Invalid email');
}
Expand All @@ -161,7 +192,11 @@ const resolvers = {
throw new Error('Invalid secret_token');
}

return await activateUser(email, secret_token);
const result = await activateUser(organization_id, email, secret_token);

return {
affected_rows: Number(result),
};
},
async auth_refresh_token(_, {}, ctx) {
const { authorization } = ctx.req.headers;
Expand Down Expand Up @@ -209,11 +244,12 @@ const resolvers = {
const accessToken = generateClaimsJwtToken(user, sessionId);

return {
accessToken,
refreshToken: generateJwtRefreshToken({
access_token: accessToken,
refresh_token: generateJwtRefreshToken({
token: newRefreshToken,
}),
userId,
organization_id: user.organization_id,
user_id: user.id,
};
},
},
Expand Down
Loading

0 comments on commit 8e8bb2e

Please sign in to comment.