Skip to content
This repository has been archived by the owner on Apr 19, 2023. It is now read-only.

Commit

Permalink
♻️ Change organization to group
Browse files Browse the repository at this point in the history
  • Loading branch information
AnandChowdhary committed Aug 2, 2020
1 parent 3dadf22 commit e7ab0eb
Show file tree
Hide file tree
Showing 28 changed files with 503 additions and 869 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[![Staart API](https://raw.githubusercontent.com/staart/staart.js.org/master/assets/svg/api.svg?sanitize=true)](https://staart.js.org/api)

Staart API is a Node.js backend starter for SaaS startups written in TypeScript. It has all the features you need to build a SaaS product, like user management and authentication, billing, organizations, GDPR tools, API keys, rate limiting, superadmin impersonation, and more.
Staart API is a Node.js backend starter for SaaS startups written in TypeScript. It has all the features you need to build a SaaS product, like user management and authentication, billing, groups, GDPR tools, API keys, rate limiting, superadmin impersonation, and more.

**⚠️ v3 BETA WARNING:** This is a fork of [Staart API](https://github.com/staart/api) with experimental changes.

Expand Down
52 changes: 25 additions & 27 deletions src/_staart/helpers/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { OrgScopes, Tokens, UserScopes, SudoScopes } from "../interfaces/enum";
import { ApiKeyResponse, AccessTokenResponse } from "./jwt";
import {
users,
organizations,
groups,
memberships,
accessTokens,
apiKeys,
} from "@prisma/client";
import { prisma } from "./prisma";
import { getUserById } from "../services/user.service";
import { getOrganizationById } from "../services/organization.service";
import { getOrganizationById } from "../services/group.service";

/**
* Whether a user can perform an action on another user
Expand Down Expand Up @@ -44,7 +44,7 @@ const canUserUser = async (user: users, action: UserScopes, target: users) => {
});

similarMemberships.forEach((similarMembership) => {
// A user can read another user in the same organization, as long as they're not a basic member
// A user can read another user in the same group, as long as they're not a basic member
if (action === UserScopes.READ_USER)
if (userMemberships[similarMembership].role) allowed = true;
});
Expand All @@ -53,7 +53,7 @@ const canUserUser = async (user: users, action: UserScopes, target: users) => {
};

/**
* Whether an access token can perform an action for an organization
* Whether an access token can perform an action for an group
*/
const canAccessTokenUser = (
accessToken: accessTokens,
Expand All @@ -70,33 +70,31 @@ const canAccessTokenUser = (
};

/**
* Whether a user can perform an action on an organization
* Whether a user can perform an action on an group
*/
const canUserOrganization = async (
user: users,
action: OrgScopes,
target: organizations
target: groups
) => {
// A super user can do anything
if (user.role === "SUDO") return true;

const memberships = await prisma.memberships.findMany({ where: { user } });
const targetMemberships = memberships.filter(
(m) => m.organizationId === target.id
);
const targetMemberships = memberships.filter((m) => m.groupId === target.id);

let allowed = false;
targetMemberships.forEach((membership) => {
// An organization owner can do anything
// An group owner can do anything
if (membership.role === "OWNER") allowed = true;

// An organization admin can do anything too
// An group admin can do anything too
if (membership.role === "ADMIN") allowed = true;

// An organization reseller can do anything too
// An group reseller can do anything too
if (membership.role === "RESELLER") allowed = true;

// An organization member can read, not edit/delete/invite
// An group member can read, not edit/delete/invite
if (
membership.role === "MEMBER" &&
(action === OrgScopes.READ_ORG ||
Expand Down Expand Up @@ -130,7 +128,7 @@ const canUserMembership = async (
memberships.forEach((membership) => {
// An admin, owner, or reseller can edit
if (
membership.organizationId === target.organizationId &&
membership.groupId === target.groupId &&
(membership.role === "OWNER" ||
membership.role === "ADMIN" ||
membership.role === "RESELLER")
Expand All @@ -139,7 +137,7 @@ const canUserMembership = async (

// Another member can view
if (
membership.organizationId === target.organizationId &&
membership.groupId === target.groupId &&
membership.role === "MEMBER" &&
action === OrgScopes.READ_ORG_MEMBERSHIPS
)
Expand All @@ -160,15 +158,15 @@ const canUserSudo = async (user: users, action: SudoScopes) => {
};

/**
* Whether an API key can perform an action for an organization
* Whether an API key can perform an action for an group
*/
const canApiKeyOrganization = (
apiKey: apiKeys,
action: OrgScopes,
target: organizations
target: groups
) => {
// An API key can only work in its own organization
if (apiKey.organizationId !== target.id) return false;
// An API key can only work in its own group
if (apiKey.groupId !== target.id) return false;

// If it has no scopes, it has no permissions
if (!apiKey.scopes) return false;
Expand All @@ -184,8 +182,8 @@ const canApiKeyOrganization = (
export const can = async (
user: string | users | ApiKeyResponse | AccessTokenResponse,
action: OrgScopes | UserScopes | SudoScopes,
targetType: "user" | "organization" | "membership" | "sudo",
target?: string | users | organizations | memberships
targetType: "user" | "group" | "membership" | "sudo",
target?: string | users | groups | memberships
) => {
let requestFromType: "users" | "apiKeys" | "accessTokens" = "users";

Expand Down Expand Up @@ -217,9 +215,9 @@ export const can = async (
});
if (!membership) throw new Error(USER_NOT_FOUND);
target = membership;
} else if (targetType === "organization") {
const organization = await getOrganizationById(target);
target = organization;
} else if (targetType === "group") {
const group = await getOrganizationById(target);
target = group;
} else {
// Target is a user
if (requestFromType === "users" && user.id === parseInt(target)) {
Expand All @@ -240,7 +238,7 @@ export const can = async (
return canApiKeyOrganization(
apiKeyDetails,
action as OrgScopes,
target as organizations
target as groups
);
} else if (requestFromType === "accessTokens") {
const accessTokenDetails = await prisma.accessTokens.findOne({
Expand All @@ -261,11 +259,11 @@ export const can = async (
action as UserScopes | OrgScopes,
target as memberships
);
} else if (targetType === "organization") {
} else if (targetType === "group") {
return canUserOrganization(
user as users,
action as OrgScopes,
target as organizations
target as groups
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/_staart/helpers/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export interface TokenResponse {
}
export interface ApiKeyResponse {
id: string;
organizationId: string;
groupId: string;
scopes: string;
jti: string;
sub: Tokens.API_KEY;
Expand Down
4 changes: 2 additions & 2 deletions src/_staart/helpers/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export const rateLimitHandler = async (
if (apiKey) {
try {
const details = await verifyToken<ApiKeyResponse>(apiKey, Tokens.API_KEY);
if (details.organizationId) {
if (details.groupId) {
res.setHeader("X-Rate-Limit-Type", "api-key");
return rateLimiter(req, res, next);
}
Expand All @@ -203,7 +203,7 @@ export const speedLimitHandler = async (
if (apiKey) {
try {
const details = await verifyToken<ApiKeyResponse>(apiKey, Tokens.API_KEY);
if (details.organizationId) {
if (details.groupId) {
res.setHeader("X-Rate-Limit-Type", "api-key");
return next();
}
Expand Down
2 changes: 1 addition & 1 deletion src/_staart/helpers/tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const trackUrl = async (req: Request, res: Response) => {
Tokens.API_KEY
);
trackingObject.apiKeyId = token.id;
trackingObject.apiKeyOrganizationId = token.organizationId;
trackingObject.apiKeyOrganizationId = token.groupId;
trackingObject.apiKeyJti = token.jti;
delete trackingObject.apiKey;
} catch (error) {
Expand Down
10 changes: 5 additions & 5 deletions src/_staart/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Tokens } from "../interfaces/enum";
import { ApiKeyResponse } from "./jwt";
import { users } from "@prisma/client";
import { prisma } from "../helpers/prisma";
import { getOrganizationById } from "../services/organization.service";
import { getOrganizationById } from "../services/group.service";
import { getUserById } from "../services/user.service";

/**
Expand All @@ -25,9 +25,9 @@ export const deleteSensitiveInfoUser = (user: users) => {
return user;
};

export const organizationUsernameToId = async (id: string) => {
export const groupUsernameToId = async (id: string) => {
const result = (
await prisma.organizations.findOne({
await prisma.groups.findOne({
select: { id: true },
where: {
username: id,
Expand Down Expand Up @@ -114,7 +114,7 @@ export const readOnlyValues = [
"id",
"jwtApiKey",
"userId",
"organizationId",
"groupId",
];

/**
Expand All @@ -123,7 +123,7 @@ export const readOnlyValues = [
export const IdValues = [
"id",
"userId",
"organizationId",
"groupId",
"primaryEmail",
"apiKeyId",
"apiKeyOrganizationId",
Expand Down
18 changes: 9 additions & 9 deletions src/_staart/helpers/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const setupQueue = async () => {
};

export const queueWebhook = (
organizationId: string,
groupId: string,
webhook: Webhooks,
data?: any
) => {
Expand All @@ -28,7 +28,7 @@ export const queueWebhook = (
redisQueue.sendMessageAsync({
qname: WEBHOOK_QUEUE,
message: JSON.stringify({
organizationId,
groupId,
webhook,
data,
tryNumber: 1,
Expand All @@ -46,30 +46,30 @@ export const receiveWebhookMessage = async () => {
});
if ("id" in result) {
const {
organizationId,
groupId,
webhook,
data,
tryNumber,
}: {
tryNumber: number;
organizationId: string;
groupId: string;
webhook: Webhooks;
data?: any;
} = JSON.parse(result.message);
if (tryNumber && tryNumber > 3) {
logError("Webhook", `Unable to fire: ${organizationId} ${webhook}`);
logError("Webhook", `Unable to fire: ${groupId} ${webhook}`);
return redisQueue.deleteMessageAsync({
qname: WEBHOOK_QUEUE,
id: result.id,
});
}
try {
safeFireWebhook(organizationId, webhook, data);
safeFireWebhook(groupId, webhook, data);
} catch (error) {
await redisQueue.sendMessageAsync({
qname: WEBHOOK_QUEUE,
message: JSON.stringify({
organizationId,
groupId,
webhook,
data,
tryNumber: tryNumber + 1,
Expand All @@ -85,12 +85,12 @@ export const receiveWebhookMessage = async () => {
};

const safeFireWebhook = async (
organizationId: string,
groupId: string,
webhook: Webhooks,
data?: any
) => {
const webhooksToFire = await prisma.webhooks.findMany({
where: { organizationId: parseInt(organizationId), event: webhook },
where: { groupId: parseInt(groupId), event: webhook },
});
for await (const hook of webhooksToFire) {
try {
Expand Down
22 changes: 11 additions & 11 deletions src/_staart/interfaces/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ export enum EventType {
AUTH_PASSWORD_CHANGED = "auth.password_changed",
AUTH_PASSWORD_RESET_REQUESTED = "auth.passwordReset",
AUTH_APPROVE_LOCATION = "auth.approveLocation",
ORGANIZATION_CREATED = "organization.created",
ORGANIZATION_UPDATED = "organization.updated",
ORGANIZATION_DELETED = "organization.deleted",
ORGANIZATION_CREATED = "group.created",
ORGANIZATION_UPDATED = "group.updated",
ORGANIZATION_DELETED = "group.deleted",
EMAIL_CREATED = "email.created",
EMAIL_UPDATED = "email.updated",
EMAIL_DELETED = "email.deleted",
Expand Down Expand Up @@ -123,14 +123,14 @@ export enum UserScopes {

export enum Webhooks {
ALL_EVENTS = "*",
UPDATE_ORGANIZATION = "update-organization",
DELETE_ORGANIZATION = "delete-organization",
UPDATE_ORGANIZATION_BILLING = "update-organization-billing",
UPDATE_ORGANIZATION_SUBSCRIPTION = "update-organization-subscription",
CREATE_ORGANIZATION_SUBSCRIPTION = "create-organization-subscription",
DELETE_ORGANIZATION_SOURCE = "delete-organization-source",
UPDATE_ORGANIZATION_SOURCE = "update-organization-source",
CREATE_ORGANIZATION_SOURCE = "create-organization-source",
UPDATE_ORGANIZATION = "update-group",
DELETE_ORGANIZATION = "delete-group",
UPDATE_ORGANIZATION_BILLING = "update-group-billing",
UPDATE_ORGANIZATION_SUBSCRIPTION = "update-group-subscription",
CREATE_ORGANIZATION_SUBSCRIPTION = "create-group-subscription",
DELETE_ORGANIZATION_SOURCE = "delete-group-source",
UPDATE_ORGANIZATION_SOURCE = "update-group-source",
CREATE_ORGANIZATION_SOURCE = "create-group-source",
UPDATE_API_KEY = "update-api-key",
CREATE_API_KEY = "create-api-key",
DELETE_API_KEY = "delete-api-key",
Expand Down
2 changes: 1 addition & 1 deletion src/_staart/interfaces/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface Event {
date?: Date;
ipAddress?: string;
userAgent?: string;
organizationId?: number | string;
groupId?: number | string;
userId?: number | string;
type?: string;
data?: any;
Expand Down
10 changes: 5 additions & 5 deletions src/_staart/rest/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import {
import { getEvents } from "@staart/payments";
import { couponCodeJwt } from "../helpers/jwt";
import {
organizationsSelect,
organizationsInclude,
organizationsOrderByInput,
organizationsWhereUniqueInput,
groupsSelect,
groupsInclude,
groupsOrderByInput,
groupsWhereUniqueInput,
usersSelect,
usersInclude,
usersOrderByInput,
Expand All @@ -32,7 +32,7 @@ export const getAllOrganizationForUser = async (
) => {
if (await can(tokenUserId, SudoScopes.READ, "sudo"))
return paginatedResult(
await prisma.organizations.findMany(queryParamsToSelect(queryParams)),
await prisma.groups.findMany(queryParamsToSelect(queryParams)),
{ first: queryParams.first, last: queryParams.last }
);
throw new Error(INSUFFICIENT_PERMISSION);
Expand Down
Loading

0 comments on commit e7ab0eb

Please sign in to comment.