fix(dashboard): creation of new org when user is apart of no orgs

This commit is contained in:
Daniel Hougaard
2024-12-16 15:55:14 +01:00
parent c00f6601bd
commit 181ba75f2a
8 changed files with 89 additions and 54 deletions
@@ -10,6 +10,7 @@ import {
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs"; import { ORGANIZATIONS } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@@ -363,21 +364,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
organization: OrganizationsSchema organization: OrganizationsSchema,
accessToken: z.string()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => { handler: async (req, res) => {
if (req.auth.actor !== ActorType.USER) return; if (req.auth.actor !== ActorType.USER) return;
const organization = await server.services.org.deleteOrganizationById( const cfg = getConfig();
req.permission.id,
req.params.organizationId, const { organization, tokens } = await server.services.org.deleteOrganizationById({
req.permission.authMethod, userId: req.permission.id,
req.permission.orgId orgId: req.params.organizationId,
); actorAuthMethod: req.permission.authMethod,
return { organization }; actorOrgId: req.permission.orgId,
authorizationHeader: req.headers.authorization,
userAgentHeader: req.headers["user-agent"],
ipAddress: req.realIp
});
void res.setCookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: cfg.HTTPS_ENABLED
});
return { organization, accessToken: tokens.accessToken };
} }
}); });
}; };
@@ -54,10 +54,11 @@ export const tokenDALFactory = (db: TDbClient) => {
const insertTokenSession = async ( const insertTokenSession = async (
userId: string, userId: string,
ip: string, ip: string,
userAgent: string userAgent: string,
tx?: Knex
): Promise<TAuthTokenSessions | undefined> => { ): Promise<TAuthTokenSessions | undefined> => {
try { try {
const [session] = await db(TableName.AuthTokenSession) const [session] = await (tx || db)(TableName.AuthTokenSession)
.insert({ .insert({
userId, userId,
ip, ip,
@@ -1,6 +1,7 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { Knex } from "knex";
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
@@ -123,14 +124,13 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
return deletedToken?.[0]; return deletedToken?.[0];
}; };
const getUserTokenSession = async ({ const getUserTokenSession = async (
userId, { userId, ip, userAgent }: TIssueAuthTokenDTO,
ip, tx?: Knex
userAgent ): Promise<TAuthTokenSessions | undefined> => {
}: TIssueAuthTokenDTO): Promise<TAuthTokenSessions | undefined> => {
let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent }); let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent });
if (!session) { if (!session) {
session = await tokenDAL.insertTokenSession(userId, ip, userAgent); session = await tokenDAL.insertTokenSession(userId, ip, userAgent, tx);
} }
return session; return session;
}; };
+32 -25
View File
@@ -1,5 +1,6 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Knex } from "knex";
import { TUsers, UserDeviceSchema } from "@app/db/schemas"; import { TUsers, UserDeviceSchema } from "@app/db/schemas";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
@@ -50,13 +51,13 @@ export const authLoginServiceFactory = ({
* Not exported. This is to update user device list * Not exported. This is to update user device list
* If new device is found. Will be saved and a mail will be send * If new device is found. Will be saved and a mail will be send
*/ */
const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string) => { const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string, tx?: Knex) => {
const devices = await UserDeviceSchema.parseAsync(user.devices || []); const devices = await UserDeviceSchema.parseAsync(user.devices || []);
const isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent); const isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent);
if (!isDeviceSeen) { if (!isDeviceSeen) {
const newDeviceList = devices.concat([{ ip, userAgent }]); const newDeviceList = devices.concat([{ ip, userAgent }]);
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }); await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }, tx);
if (user.email) { if (user.email) {
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.NewDeviceJoin, template: SmtpTemplates.NewDeviceJoin,
@@ -97,30 +98,36 @@ export const authLoginServiceFactory = ({
* Check user device and send mail if new device * Check user device and send mail if new device
* generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled * generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled
*/ */
const generateUserTokens = async ({ const generateUserTokens = async (
user, {
ip, user,
userAgent,
organizationId,
authMethod,
isMfaVerified,
mfaMethod
}: {
user: TUsers;
ip: string;
userAgent: string;
organizationId?: string;
authMethod: AuthMethod;
isMfaVerified?: boolean;
mfaMethod?: MfaMethod;
}) => {
const cfg = getConfig();
await updateUserDeviceSession(user, ip, userAgent);
const tokenSession = await tokenService.getUserTokenSession({
userAgent,
ip, ip,
userId: user.id userAgent,
}); organizationId,
authMethod,
isMfaVerified,
mfaMethod
}: {
user: TUsers;
ip: string;
userAgent: string;
organizationId?: string;
authMethod: AuthMethod;
isMfaVerified?: boolean;
mfaMethod?: MfaMethod;
},
tx?: Knex
) => {
const cfg = getConfig();
await updateUserDeviceSession(user, ip, userAgent, tx);
const tokenSession = await tokenService.getUserTokenSession(
{
userAgent,
ip,
userId: user.id
},
tx
);
if (!tokenSession) throw new Error("Failed to create token"); if (!tokenSession) throw new Error("Failed to create token");
const accessToken = jwt.sign( const accessToken = jwt.sign(
+7 -2
View File
@@ -77,11 +77,16 @@ export const selectOrganization = async (data: {
export const useSelectOrganization = () => { export const useSelectOrganization = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => { mutationFn: async (details: {
organizationId: string;
userAgent?: UserAgentType;
forceSetCredentials?: boolean;
}) => {
const data = await selectOrganization(details); const data = await selectOrganization(details);
// If a custom user agent is set, then this session is meant for another consuming application, not the web application. // If a custom user agent is set, then this session is meant for another consuming application, not the web application.
if (!details.userAgent && !data.isMfaEnabled) { if ((!details.userAgent && !data.isMfaEnabled) || details.forceSetCredentials) {
localStorage.setItem("orgData.id", details.organizationId);
SecurityClient.setToken(data.token); SecurityClient.setToken(data.token);
SecurityClient.setProviderAuthToken(""); SecurityClient.setProviderAuthToken("");
} }
@@ -1,5 +1,6 @@
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
@@ -67,7 +68,7 @@ export const useCreateOrg = (options: { invalidate: boolean } = { invalidate: tr
mutationFn: async ({ name }: { name: string }) => { mutationFn: async ({ name }: { name: string }) => {
const { const {
data: { organization } data: { organization }
} = await apiRequest.post("/api/v2/organizations", { } = await apiRequest.post<{ organization: { id: string } }>("/api/v2/organizations", {
name name
}); });
@@ -437,10 +438,13 @@ export const useDeleteOrgById = () => {
return useMutation({ return useMutation({
mutationFn: async ({ organizationId }: { organizationId: string }) => { mutationFn: async ({ organizationId }: { organizationId: string }) => {
const { const {
data: { organization } data: { organization, accessToken }
} = await apiRequest.delete<{ organization: Organization }>( } = await apiRequest.delete<{ organization: Organization; accessToken: string }>(
`/api/v2/organizations/${organizationId}` `/api/v2/organizations/${organizationId}`
); );
SecurityClient.setToken(accessToken);
localStorage.removeItem("orgData.id");
return organization; return organization;
}, },
onSuccess(_, dto) { onSuccess(_, dto) {
+1 -1
View File
@@ -7,7 +7,7 @@ import { ProjectType } from "@app/hooks/api/workspace/types";
import { queryClient } from "@app/reactQuery"; import { queryClient } from "@app/reactQuery";
export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => { export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => {
const userOrgs = await fetchOrganizations(); const userOrgs = await fetchOrganizations().catch(() => []);
const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced); const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced);
@@ -6,7 +6,7 @@ import z from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api"; import { useCreateOrg, useGetOrganizations, useSelectOrganization } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
const schema = z const schema = z
@@ -23,9 +23,10 @@ interface CreateOrgModalProps {
} }
export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) => { export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) => {
const router = useRouter(); const router = useRouter();
const { refetch: refetchOrganizations } = useGetOrganizations();
const { const {
control, control,
handleSubmit, handleSubmit,
@@ -50,19 +51,21 @@ export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) =>
}); });
await selectOrg({ await selectOrg({
organizationId: organization.id organizationId: organization.id,
forceSetCredentials: true
}); });
await refetchOrganizations();
createNotification({ createNotification({
text: "Successfully created organization", text: "Successfully created organization",
type: "success" type: "success"
}); });
if (router.isReady) router.push(`/org/${organization.id}/${ProjectType.SecretManager}/overview`); if (router.isReady)
router.push(`/org/${organization.id}/${ProjectType.SecretManager}/overview`);
else window.location.href = `/org/${organization.id}/${ProjectType.SecretManager}/overview`; else window.location.href = `/org/${organization.id}/${ProjectType.SecretManager}/overview`;
localStorage.setItem("orgData.id", organization.id);
reset(); reset();
onClose(); onClose();
} catch (err) { } catch (err) {