diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index cb630b143..8ca105ad4 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -10,6 +10,7 @@ import { UsersSchema } from "@app/db/schemas"; import { ORGANIZATIONS } from "@app/lib/api-docs"; +import { getConfig } from "@app/lib/config/env"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; @@ -363,21 +364,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - organization: OrganizationsSchema + organization: OrganizationsSchema, + accessToken: z.string() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), - handler: async (req) => { + handler: async (req, res) => { if (req.auth.actor !== ActorType.USER) return; - const organization = await server.services.org.deleteOrganizationById( - req.permission.id, - req.params.organizationId, - req.permission.authMethod, - req.permission.orgId - ); - return { organization }; + const cfg = getConfig(); + + const { organization, tokens } = await server.services.org.deleteOrganizationById({ + userId: req.permission.id, + orgId: req.params.organizationId, + actorAuthMethod: req.permission.authMethod, + 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 }; } }); }; diff --git a/backend/src/services/auth-token/auth-token-dal.ts b/backend/src/services/auth-token/auth-token-dal.ts index c058c13e8..ed45d505e 100644 --- a/backend/src/services/auth-token/auth-token-dal.ts +++ b/backend/src/services/auth-token/auth-token-dal.ts @@ -54,10 +54,11 @@ export const tokenDALFactory = (db: TDbClient) => { const insertTokenSession = async ( userId: string, ip: string, - userAgent: string + userAgent: string, + tx?: Knex ): Promise => { try { - const [session] = await db(TableName.AuthTokenSession) + const [session] = await (tx || db)(TableName.AuthTokenSession) .insert({ userId, ip, diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 321abb5b3..b9fc98106 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -1,6 +1,7 @@ import crypto from "node:crypto"; import bcrypt from "bcrypt"; +import { Knex } from "knex"; import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; @@ -123,14 +124,13 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu return deletedToken?.[0]; }; - const getUserTokenSession = async ({ - userId, - ip, - userAgent - }: TIssueAuthTokenDTO): Promise => { + const getUserTokenSession = async ( + { userId, ip, userAgent }: TIssueAuthTokenDTO, + tx?: Knex + ): Promise => { let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent }); if (!session) { - session = await tokenDAL.insertTokenSession(userId, ip, userAgent); + session = await tokenDAL.insertTokenSession(userId, ip, userAgent, tx); } return session; }; diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index dea41e60b..8dfe69643 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -1,5 +1,6 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; +import { Knex } from "knex"; import { TUsers, UserDeviceSchema } from "@app/db/schemas"; 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 * 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 isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent); if (!isDeviceSeen) { 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) { await smtpService.sendMail({ template: SmtpTemplates.NewDeviceJoin, @@ -97,30 +98,36 @@ export const authLoginServiceFactory = ({ * 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 */ - const generateUserTokens = async ({ - user, - ip, - 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, + const generateUserTokens = async ( + { + user, 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"); const accessToken = jwt.sign( diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index de3c60d46..0c3dbe0d9 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -77,11 +77,16 @@ export const selectOrganization = async (data: { export const useSelectOrganization = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => { + mutationFn: async (details: { + organizationId: string; + userAgent?: UserAgentType; + forceSetCredentials?: boolean; + }) => { 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 (!details.userAgent && !data.isMfaEnabled) { + if ((!details.userAgent && !data.isMfaEnabled) || details.forceSetCredentials) { + localStorage.setItem("orgData.id", details.organizationId); SecurityClient.setToken(data.token); SecurityClient.setProviderAuthToken(""); } diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 4923177ba..82894d988 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -1,5 +1,6 @@ import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query"; +import SecurityClient from "@app/components/utilities/SecurityClient"; import { apiRequest } from "@app/config/request"; 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 }) => { const { data: { organization } - } = await apiRequest.post("/api/v2/organizations", { + } = await apiRequest.post<{ organization: { id: string } }>("/api/v2/organizations", { name }); @@ -437,10 +438,13 @@ export const useDeleteOrgById = () => { return useMutation({ mutationFn: async ({ organizationId }: { organizationId: string }) => { const { - data: { organization } - } = await apiRequest.delete<{ organization: Organization }>( + data: { organization, accessToken } + } = await apiRequest.delete<{ organization: Organization; accessToken: string }>( `/api/v2/organizations/${organizationId}` ); + SecurityClient.setToken(accessToken); + localStorage.removeItem("orgData.id"); + return organization; }, onSuccess(_, dto) { diff --git a/frontend/src/views/Login/Login.utils.tsx b/frontend/src/views/Login/Login.utils.tsx index dc714e4b0..0562d383a 100644 --- a/frontend/src/views/Login/Login.utils.tsx +++ b/frontend/src/views/Login/Login.utils.tsx @@ -7,7 +7,7 @@ import { ProjectType } from "@app/hooks/api/workspace/types"; import { queryClient } from "@app/reactQuery"; export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => { - const userOrgs = await fetchOrganizations(); + const userOrgs = await fetchOrganizations().catch(() => []); const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced); diff --git a/frontend/src/views/Org/components/CreateOrgModal.tsx b/frontend/src/views/Org/components/CreateOrgModal.tsx index d1baf2c9c..faa02eccd 100644 --- a/frontend/src/views/Org/components/CreateOrgModal.tsx +++ b/frontend/src/views/Org/components/CreateOrgModal.tsx @@ -6,7 +6,7 @@ import z from "zod"; import { createNotification } from "@app/components/notifications"; 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"; const schema = z @@ -23,9 +23,10 @@ interface CreateOrgModalProps { } export const CreateOrgModal: FC = ({ isOpen, onClose }) => { - const router = useRouter(); + const { refetch: refetchOrganizations } = useGetOrganizations(); + const { control, handleSubmit, @@ -50,19 +51,21 @@ export const CreateOrgModal: FC = ({ isOpen, onClose }) => }); await selectOrg({ - organizationId: organization.id + organizationId: organization.id, + forceSetCredentials: true }); + await refetchOrganizations(); + createNotification({ text: "Successfully created organization", 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`; - localStorage.setItem("orgData.id", organization.id); - reset(); onClose(); } catch (err) {