diff --git a/.gitignore b/.gitignore index 07d1ff8d3..ff59d7935 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ # Mono auto generated files mono_crash.* +#App JSON file +app.json + # Build results [Dd]ebug/ [Dd]ebugPublic/ diff --git a/.talismanrc b/.talismanrc index 4c4dc6cff..9fc162a4a 100644 --- a/.talismanrc +++ b/.talismanrc @@ -158,4 +158,35 @@ fileignoreconfig: checksum: f3bd8c6e981ed0acf26432859b2b7e388c0d90018513005cfc674726f14fe245 - filename: ui/src/components/SchemaModal/index.tsx checksum: 607a465c9cd4a504b9a81750a3f9faa0f4e11c09414354d69ec7308c11f0046a + +fileignoreconfig: +- filename: api/sso.utils.js + checksum: 5d589c128c4b38f8aacd70e5d02ddd7fa8e93ff7897ca69a1258378139d1d616 +version: "1.0" + +fileignoreconfig: +- filename: api/package-lock.json + checksum: 4d2fd1905b5933e1d2c4d178e1536422d4aac84caa9640149eab0432a75b712d +- filename: api/src/services/migration.service.ts + checksum: 1fdf5423840e170709c7c677c3a6a7c6ae61f373948c2ef295aa645a859c1af5 +- filename: api/src/services/contentMapper.service.ts + checksum: 03d5dcc31b38fd435f6a4389d6891c7fc1ba27b32dc2b382b91173d84f4565f7 +- filename: api/src/services/globalField.service.ts + checksum: b808815c7372f68fe9a5904d23be50cb0ec066592328ec1721dc3c395cbe3a2c +- filename: api/src/services/taxonomy.service.ts + checksum: 840ab11838ebf08df44ada0a3674dad8cc124bc8bcbc5dfd1d9c585a34e4aeda +- filename: api/src/services/org.service.ts + checksum: 0a50297164d7845d889fc78097164c4794a3f9cd7314c06365c8426a2a6ee52a +- filename: ui/src/pages/Login/index.tsx + checksum: 7f7c008586db60f1cc8df625b88bfdc5c3bb861c21e40a55fc763f0ac4a6a8d2 +version: "1.0" + +fileignoreconfig: +- filename: api/src/services/contentMapper.service.ts + checksum: 924b124214a93a7bec4c471304f5b270d5e735d506644180273b7118f3d37dd2 +version: "1.0" + +fileignoreconfig: +- filename: ui/src/pages/Login/index.tsx + checksum: 213c6441dc87d82ce6b97679d457ae56c6e40ef13a89bddd4f21afcf566b5576 version: "1.0" \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore index 736178335..a3563aa2f 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -366,5 +366,4 @@ database/ /cmsMigrationData /migration-data **/copy* -**copy.ts -manifest.json \ No newline at end of file +**copy.ts \ No newline at end of file diff --git a/api/package-lock.json b/api/package-lock.json index e7eed4099..d42bacaaf 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -43,6 +43,7 @@ "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/express-session": "^1.18.2", "@types/fs-extra": "^11.0.4", "@types/fs-readdir-recursive": "^1.1.3", "@types/jsdom": "^21.1.7", @@ -2384,7 +2385,7 @@ "yarn": "^1.22.22" }, "engines": { - "node": ">=18.0.0" + "node": ">=14" } }, "node_modules/@otplib/core": { @@ -2973,6 +2974,15 @@ "@types/send": "*" } }, + "node_modules/@types/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/fs-extra": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", @@ -5579,6 +5589,15 @@ "node": ">= 0.6" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -5723,6 +5742,50 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/express-validator": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", @@ -9218,6 +9281,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -11952,6 +12023,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -12647,6 +12726,14 @@ } ] }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -14350,6 +14437,17 @@ "node": ">=14.17" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/api/package.json b/api/package.json index 48c10dba4..d282896d6 100644 --- a/api/package.json +++ b/api/package.json @@ -13,7 +13,6 @@ "lint:fix": "eslint --ext .ts --ignore-pattern './node_modules/' --ignore-pattern './dist/'", "precommit": "npm run prettify && npm run lint:fix" }, - "type": "module", "repository": { "type": "git", "url": "git+https://github.com/contentstack/migration-v2.git" @@ -59,6 +58,7 @@ "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/express-session": "^1.18.2", "@types/fs-extra": "^11.0.4", "@types/fs-readdir-recursive": "^1.1.3", "@types/jsdom": "^21.1.7", diff --git a/api/src/constants/index.ts b/api/src/constants/index.ts index 777712bd6..e44f52010 100644 --- a/api/src/constants/index.ts +++ b/api/src/constants/index.ts @@ -8,6 +8,21 @@ export const DEVURLS: any = { AU: "au-developerhub-api.contentstack.com", GCP_EU: "gcp-eu-developerhub-api.contentstack.com", }; +export const CSAUTHHOST: any = { + NA:"https://app.contentstack.com/apps-api/token", + EU:"https://eu-app.contentstack.com/apps-api/token", + AZURE_NA:"https://azure-na-app.contentstack.com/apps-api/token", + AZURE_EU:"https://azure-eu-app.contentstack.com/apps-api/token", + GCP_NA:"https://gcp-na-app.contentstack.com/apps-api/token", +} + +export const regionalApiHosts = { + NA: 'api.contentstack.io', + EU: 'eu-api.contentstack.com', + AZURE_NA: 'azure-na-api.contentstack.com', + AZURE_EU: 'azure-eu-api.contentstack.com', + GCP_NA: 'gcp-na-api.contentstack.com' +}; export const CMS = { CONTENTFUL: "contentful", SITECORE_V8: "sitecore v8", diff --git a/api/src/controllers/auth.controller.ts b/api/src/controllers/auth.controller.ts index bdcbe1804..abf01775d 100644 --- a/api/src/controllers/auth.controller.ts +++ b/api/src/controllers/auth.controller.ts @@ -1,5 +1,6 @@ import { Request, Response } from "express"; import { authService } from "../services/auth.service.js"; +import { HTTP_CODES } from "../constants/index.js"; /** * Handles the login request. @@ -23,7 +24,107 @@ const RequestSms = async (req: Request, res: Response) => { res.status(resp.status).json(resp.data); }; + +/** + * Generates the OAuth token and saves it to the database. + * @param req - The request object. Sends the code and region. + * @param res - The response object. Sends the message "Token received successfully." + */ +const saveOAuthToken = async (req: Request, res: Response) => { + await authService.saveOAuthToken(req); + res.status(HTTP_CODES.OK).json({ message: "Token received successfully." }); +}; + + +/** + * Handles the request for getting the app configuration. + * + * @param req - The request object. + * @param res - The response object. + */ +export const getAppConfigHandler = async (req: Request, res: Response): Promise => { + try { + const appConfig = await authService.getAppData(); + res.status(200).json(appConfig); + + } catch (error: any) { + console.error('Error in getAppConfig controller:', error); + + if (error?.message?.includes('app.json file not found')) { + res.status(404).json({ + error: 'SSO configuration not found', + message: 'app.json file does not exist' + }); + return; + } + + if (error?.message?.includes('Invalid JSON format')) { + res.status(400).json({ + error: 'Invalid SSO configuration', + message: 'app.json contains invalid JSON' + }); + return; + } + + res.status(500).json({ + error: 'Server error', + message: 'Unable to read SSO configuration' + }); + } +}; + +/** + * Handles the request for checking the SSO authentication status. + * + * @param req - The request object. + * @param res - The response object. + */ +export const getSSOAuthStatus = async (req: Request, res: Response): Promise => { + try { + const { userId } = req.params; + + if (!userId) { + res.status(400).json({ + error: 'Missing user ID', + message: 'User ID parameter is required' + }); + return; + } + + const authStatus = await authService.checkSSOAuthStatus(userId); + + res.status(200).json(authStatus); + + } catch (error: any) { + console.error('Error in getSSOAuthStatus controller:', error); + + if (error?.message?.includes('User not found in authentication records')) { + res.status(404).json({ + error: 'User not found', + message: 'User not found in authentication records' + }); + return; + } + + if (error?.message?.includes('SSO authentication not completed')) { + res.status(202).json({ + authenticated: false, + message: 'SSO authentication not completed' + }); + return; + } + + res.status(500).json({ + error: 'Server error', + message: 'Unable to check SSO authentication status' + }); + } +}; + export const authController = { login, RequestSms, + saveOAuthToken, + getAppConfigHandler, + getSSOAuthStatus }; diff --git a/api/src/models/authentication.ts b/api/src/models/authentication.ts index fcbefe733..48dc209f2 100644 --- a/api/src/models/authentication.ts +++ b/api/src/models/authentication.ts @@ -13,6 +13,7 @@ interface AuthenticationDocument { authtoken: string; created_at: string; updated_at: string; + access_token: string; }[]; } diff --git a/api/src/models/types.ts b/api/src/models/types.ts index 43e7eb049..a8409efcf 100644 --- a/api/src/models/types.ts +++ b/api/src/models/types.ts @@ -19,6 +19,7 @@ export interface User { export interface AppTokenPayload { region: string; user_id: string; + is_sso: boolean; } /** @@ -45,4 +46,11 @@ export interface Locale { name: string; fallback_locale: string; uid: string; +} + +export interface RefreshTokenResponse { + access_token: string; + refresh_token?: string; + expires_in?: number; + token_type?: string; } \ No newline at end of file diff --git a/api/src/routes/auth.routes.ts b/api/src/routes/auth.routes.ts index 60b1755b9..322243aac 100644 --- a/api/src/routes/auth.routes.ts +++ b/api/src/routes/auth.routes.ts @@ -40,4 +40,30 @@ router.post( asyncRouter(authController.RequestSms) ); +/** + * Generates the OAuth token and saves it to the database. + * @param req - The request object. Sends the code and region. + * @param res - The response object. Sends the message "Token received successfully." + * @route POST /v2/auth/save-token + */ +router.get( + "/save-token", + asyncRouter(authController.saveOAuthToken) +); + +/** + * @route GET /api/app-config + * @desc Get app configuration from app.json + * @access Public + */ +router.get('/app-config', authController.getAppConfigHandler); + +/** + * @route GET /v2/auth/sso-status/:userId + * @desc Check SSO authentication status for a user + * @param userId - The user ID to check authentication status for + * @access Public + */ +router.get('/sso-status/:userId', authController.getSSOAuthStatus); + export default router; diff --git a/api/src/services/auth.service.ts b/api/src/services/auth.service.ts index cf512a621..3310e3854 100644 --- a/api/src/services/auth.service.ts +++ b/api/src/services/auth.service.ts @@ -2,8 +2,8 @@ import { Request } from "express"; import { config } from "../config/index.js"; import { safePromise, getLogMessage } from "../utils/index.js"; import https from "../utils/https.utils.js"; -import { LoginServiceType, AppTokenPayload } from "../models/types.js"; -import { HTTP_CODES, HTTP_TEXTS } from "../constants/index.js"; +import { LoginServiceType, AppTokenPayload, RefreshTokenResponse } from "../models/types.js"; +import { HTTP_CODES, HTTP_TEXTS, CSAUTHHOST, regionalApiHosts } from "../constants/index.js"; import { generateToken } from "../utils/jwt.utils.js"; import { BadRequestError, @@ -12,23 +12,16 @@ import { } from "../utils/custom-errors.utils.js"; import AuthenticationModel from "../models/authentication.js"; import logger from "../utils/logger.js"; -// import * as configHandler from "@contentstack/cli-utilities"; +import path from "path"; +import fs from "fs"; +import axios from "axios"; +// import { createHash, randomBytes } from 'crypto'; /** - * Logs in a user with the provided request data. - * - * @param req - The request object containing user data. - * @returns A promise that resolves to a LoginServiceType object. - * @throws ExceptionFunction if an error occurs during the login process. + * Logs in a user with the provided request data. (No changes needed here) */ const login = async (req: Request): Promise => { const srcFun = "Login"; - /* - handles user authentication by making a request to an API, - performing various checks and validations, - updating a model, and generating a JWT token. - It also handles potential errors and logs appropriate messages. - */ try { const userData = req?.body; @@ -90,6 +83,7 @@ const login = async (req: Request): Promise => { const appTokenPayload: AppTokenPayload = { region: userData?.region, user_id: res?.data?.user.uid, + is_sso: false, }; // Saving auth info in the DB @@ -135,19 +129,10 @@ const login = async (req: Request): Promise => { }; /** - * Sends a request for SMS login token. - * @param req - The request object. - * @returns A promise that resolves to a LoginServiceType object. - * @throws {InternalServerError} If an error occurs while sending the request. + * Sends a request for SMS login token. (No changes needed here) */ const requestSms = async (req: Request): Promise => { const srcFun = "requestSms"; - - /* - handles the authentication process by making an HTTP POST request to an API endpoint, - handling any errors that occur, and returning the appropriate response or error data. - It also includes logging functionality to track the execution and potential errors. - */ try { const userData = req?.body; const [err, res] = await safePromise( @@ -187,7 +172,272 @@ const requestSms = async (req: Request): Promise => { } }; +const getAppConfig = () => { + const configPath = path.resolve(process.cwd(), '..', 'app.json'); + if (!fs.existsSync(configPath)) { + throw new InternalServerError("SSO is not configured. Please run the setup script first."); + } + const rawData = fs.readFileSync(configPath, 'utf-8'); + return JSON.parse(rawData); +}; + +/** + * Receives the final code to generate token, fetches user details, + * and saves/updates the user in the database. + */ +const saveOAuthToken = async (req: Request): Promise => { + const { code, region } = req.query; + + if (!code || !region) { + logger.error("Callback failed: Missing 'code' or 'region' in query parameters."); + throw new BadRequestError("Missing 'code' or 'region' in query parameters."); + } + + try { + // Exchange the code for access token + const appConfig = getAppConfig(); + const { client_id, client_secret, redirect_uri } = appConfig.oauthData; + const { code_verifier } = appConfig.pkce; + + const regionStr = Array.isArray(region) ? region[0] : region; + const tokenUrl = CSAUTHHOST[regionStr as keyof typeof CSAUTHHOST]; + if (!tokenUrl || !client_id || !client_secret) { + throw new InternalServerError(`Configuration missing for region: ${region}`); + } + + const formData = new URLSearchParams(); + formData.append('grant_type', 'authorization_code'); + formData.append('client_id', client_id); + formData.append('client_secret', client_secret); + formData.append('redirect_uri', redirect_uri); + formData.append('code', code as string); + formData.append('code_verifier', code_verifier); + const tokenResponse = await https({ + method: "POST", + url: tokenUrl, + data: formData, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } + }); + + const { access_token, refresh_token, organization_uid } = tokenResponse.data; + + const apiHost = regionalApiHosts[region as keyof typeof regionalApiHosts]; + const [userErr, userRes] = await safePromise( + https({ + method: "GET", + url: `https://${apiHost}/v3/user`, + headers: { + 'authorization': `Bearer ${access_token}`, + }, + }) + ); + + if (userErr) { + logger.error("Error fetching user details with new token", userErr?.response?.data); + throw new InternalServerError(userErr); + } + + const csUser = userRes?.data?.user; + + const appTokenPayload = { + region: region as string, + user_id: csUser?.uid, + is_sso: true, + }; + + const appToken = generateToken(appTokenPayload); + await AuthenticationModel.read(); + const userIndex = AuthenticationModel.chain.get("users").findIndex({ user_id: csUser?.uid }).value(); + + AuthenticationModel.update((data: any) => { + const userRecord = { + ...appTokenPayload, + email: csUser?.email, + access_token: access_token, + refresh_token: refresh_token, + organization_uid: organization_uid, + updated_at: new Date().toISOString(), + }; + if (userIndex < 0) { + data.users.push({ ...userRecord, created_at: new Date().toISOString() }); + } else { + data.users[userIndex] = { ...data.users[userIndex], ...userRecord }; + } + }); + + logger.info(`Token and user data for ${csUser.email} (Region: ${region}) saved successfully.`); + return { + data: { + message: HTTP_TEXTS.SUCCESS_LOGIN, + app_token: appToken, + }, + status: HTTP_CODES.OK, + } + + } catch (error) { + logger.error("An error occurred during token exchange and save:", error); + throw new InternalServerError("Failed to process OAuth callback."); + } +}; + +/** + * Generates a new access token using the refresh token. + * If the refresh token is not found, it throws an error. + * It updates the user record in the database with the new access token and refresh token. + * It returns the new access token. + */ +export const refreshOAuthToken = async (userId: string): Promise => { + try { + await AuthenticationModel.read(); + const userRecord = AuthenticationModel.chain.get("users").find({ user_id: userId }).value(); + + if (!userRecord) { + throw new Error(`User record not found for user_id: ${userId}`); + } + + if (!userRecord?.refresh_token) { + throw new Error(`No refresh token available for user: ${userId}`); + } + + const appConfigPath = path.join(process.cwd(), "..", 'app.json'); + if (!fs.existsSync(appConfigPath)) { + throw new Error('app.json file not found - OAuth configuration required'); + } + + const appConfig = JSON.parse(fs.readFileSync(appConfigPath, 'utf8')); + const { client_id, client_secret, redirect_uri } = appConfig.oauthData; + + if (!client_id || !client_secret) { + throw new Error('OAuth client_id or client_secret not found in app.json'); + } + + logger.info(`Refreshing token for user: ${userRecord?.email} in region: ${userRecord?.region}`); + + const appUrl = CSAUTHHOST[userRecord.region] || CSAUTHHOST['NA']; + const tokenEndpoint = `${appUrl}`; + + const formData = new URLSearchParams({ + grant_type: 'refresh_token', + client_id: client_id, + client_secret: client_secret, + redirect_uri: redirect_uri, + refresh_token: userRecord?.refresh_token + }); + + const response = await axios.post(tokenEndpoint, formData, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + timeout: 15000 + }); + + const { access_token, refresh_token } = response?.data; + + AuthenticationModel.update((data: any) => { + const userIndex = data?.users?.findIndex((user: any) => user?.user_id === userId); + if (userIndex >= 0) { + data.users[userIndex] = { + ...data?.users[userIndex], + access_token: access_token, + refresh_token: refresh_token || userRecord.refresh_token, + updated_at: new Date().toISOString() + }; + } + }); + + logger.info(`Token refreshed successfully for user: ${userRecord?.email}`); + return access_token; + + } catch (error: any) { + logger.error(`Token refresh failed for user ${userId}:`, error?.response?.data || error?.message); + throw new Error(`Failed to refresh token: ${error.response?.data?.error_description || error.message}`); + } +}; + +/** + * Check app.json file for SSO configuration. + * @returns The app configuration + */ +export const getAppData = async () => { + try { + const appConfigPath = path.join(process.cwd(), '..','app.json'); + + if (!fs.existsSync(appConfigPath)) { + throw new Error('app.json file not found - SSO configuration required'); + } + + const appConfigData = fs.readFileSync(appConfigPath, 'utf8'); + const appConfig: any = JSON.parse(appConfigData); + + return appConfig; + + } catch (error: any) { + if (error?.message?.includes('app.json file not found')) { + throw error; + } + if (error instanceof SyntaxError) { + throw new Error('Invalid JSON format in app.json file'); + } + throw new Error(`Failed to read app configuration: ${error?.message}`); + } +} + +/** + * Checks the status of the SSO authentication. + * @param userId - The user ID + * @returns The authentication status + */ +export const checkSSOAuthStatus = async (userId: string) => { + try { + await AuthenticationModel.read(); + const userRecord = AuthenticationModel.chain.get("users").find({ user_id: userId }).value(); + + if (!userRecord || !userRecord?.access_token) { + throw new Error('SSO authentication not completed'); + } + + // Only consider tokens created in the last 10 minutes as "fresh" + const tokenAge = new Date()?.getTime() - new Date(userRecord?.updated_at)?.getTime(); + const maxTokenAge = 10 * 60 * 1000; // 10 minutes in milliseconds + + if (tokenAge > maxTokenAge) { + throw new Error('SSO authentication not completed'); + } + + // Token is fresh, proceed with authentication + const appTokenPayload = { + region: userRecord?.region, + user_id: userRecord.user_id, + is_sso: true, + }; + + const appToken = generateToken(appTokenPayload); + + return { + authenticated: true, + message: 'SSO authentication successful', + app_token: appToken, + user: { + email: userRecord?.email, + uid: userRecord?.user_id, + region: userRecord?.region, + organization_uid: userRecord?.organization_uid + } + }; + + } catch (error: any) { + if (error?.message?.includes('SSO authentication not completed')) { + throw error; + } + throw new Error(`Failed to check SSO authentication status: ${error?.message}`); + } +}; + export const authService = { login, requestSms, -}; + saveOAuthToken, + refreshOAuthToken, + getAppData, + checkSSOAuthStatus +}; \ No newline at end of file diff --git a/api/src/services/contentMapper.service.ts b/api/src/services/contentMapper.service.ts index 76a819297..22b3dfccc 100644 --- a/api/src/services/contentMapper.service.ts +++ b/api/src/services/contentMapper.service.ts @@ -15,7 +15,7 @@ import { import logger from "../utils/logger.js"; import { config } from "../config/index.js"; import https from "../utils/https.utils.js"; -import getAuthtoken from "../utils/auth.utils.js"; +import getAuthtoken, { getAccessToken } from "../utils/auth.utils.js"; import getProjectUtil from "../utils/get-project.utils.js"; import fetchAllPaginatedData from "../utils/pagination.utils.js"; import ProjectModelLowdb from "../models/project-lowdb.js"; @@ -366,26 +366,29 @@ const getExistingContentTypes = async (req: Request) => { const { token_payload } = req.body; - const authtoken = await getAuthtoken( - token_payload?.region, - token_payload?.user_id - ); await ProjectModelLowdb.read(); const project = ProjectModelLowdb.chain .get("projects") .find({ id: projectId }) .value(); - const stackId = project?.destination_stack_id; const baseUrl = `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/content_types`; - - const headers = { - api_key: stackId, - authtoken, - }; + let headers: any = { + api_key: project?.destination_stack_id, + } + if(token_payload?.is_sso) { + const accessToken = await getAccessToken(token_payload?.region, token_payload?.user_id); + headers.authorization = `Bearer ${accessToken}`; + } else if (token_payload?.is_sso === false) { + const authtoken = await getAuthtoken( + token_payload?.region, + token_payload?.user_id + ); + headers.authtoken = authtoken; + } try { // Step 1: Fetch the updated list of all content types diff --git a/api/src/services/globalField.service.ts b/api/src/services/globalField.service.ts index 950a5b8de..0a28d79a0 100644 --- a/api/src/services/globalField.service.ts +++ b/api/src/services/globalField.service.ts @@ -1,11 +1,11 @@ import { getLogMessage, safePromise } from "../utils/index.js"; -import getAuthtoken from "../utils/auth.utils.js"; import { config } from "../config/index.js"; import https from "../utils/https.utils.js"; import fs from 'fs'; import { HTTP_TEXTS, MIGRATION_DATA_CONFIG} from "../constants/index.js"; import path from "path"; import logger from "../utils/logger.js"; +import AuthenticationModel from "../models/authentication.js"; const { GLOBAL_FIELDS_FILE_NAME, @@ -25,7 +25,26 @@ const createGlobalField = async ({ current_test_stack_id?: string; }) => { const srcFun = "createGlobalField"; - const authtoken = await getAuthtoken(region, user_id); + let headers: any = { + api_key : stackId, + } + let authtoken = ""; + await AuthenticationModel.read(); + const userIndex = AuthenticationModel.chain + .get('users') + .findIndex({ region, user_id: user_id }) + .value(); + + const userData = AuthenticationModel?.data?.users[userIndex]; + if(userData?.access_token) { + authtoken = `Bearer ${userData?.access_token}`; + headers.authorization = authtoken; + } else if(userData?.authtoken) { + authtoken = userData?.authtoken; + headers.authtoken = authtoken; + }else{ + throw new Error("No authentication token found"); + } try { const [err, res] = await safePromise( https({ @@ -33,10 +52,7 @@ const createGlobalField = async ({ url: `${config.CS_API[ region as keyof typeof config.CS_API ]!}/global_fields?include_global_field_schema=true`, - headers: { - api_key : stackId, - authtoken, - }, + headers: headers, }) ); const globalSave = path.join(MIGRATION_DATA_CONFIG.DATA, current_test_stack_id ?? '', GLOBAL_FIELDS_DIR_NAME); diff --git a/api/src/services/marketplace.service.ts b/api/src/services/marketplace.service.ts index 06a010483..d317b8653 100644 --- a/api/src/services/marketplace.service.ts +++ b/api/src/services/marketplace.service.ts @@ -1,9 +1,10 @@ import path from 'path'; import fs from 'fs'; -import getAuthtoken from '../utils/auth.utils.js'; import { MIGRATION_DATA_CONFIG, KEYTOREMOVE } from '../constants/index.js'; import { getAppManifestAndAppConfig } from '../utils/market-app.utils.js'; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from "uuid"; +import AuthenticationModel from "../models/authentication.js"; + const { EXTENSIONS_MAPPER_DIR_NAME, @@ -51,21 +52,27 @@ const writeManifestFile = async ({ destinationStackId, appManifest }: any) => { } }; -const createAppManifest = async ({ - destinationStackId, - region, - userId, - orgId, -}: any) => { - const authtoken = await getAuthtoken(region, userId); - const marketPlacePath = path.join( - MIGRATION_DATA_CONFIG.DATA, - destinationStackId, - EXTENSIONS_MAPPER_DIR_NAME - ); - const AppMapper: any = await fs.promises - .readFile(marketPlacePath, 'utf-8') - .catch(async () => {}); + + +const createAppManifest = async ({ destinationStackId, region, userId, orgId }: any) => { + let authtoken = ""; + await AuthenticationModel.read(); + const userIndex = AuthenticationModel.chain + .get('users') + .findIndex({ region, user_id: userId }) + .value(); + + const userData = AuthenticationModel?.data?.users[userIndex]; + if(userData?.access_token) { + + authtoken = `Bearer ${userData?.access_token}`; + } else if(userData?.authtoken) { + authtoken = userData?.authtoken; + }else{ + throw new Error("No authentication token found"); + } + const marketPlacePath = path.join(MIGRATION_DATA_CONFIG.DATA, destinationStackId, EXTENSIONS_MAPPER_DIR_NAME); + const AppMapper: any = await fs.promises.readFile(marketPlacePath, "utf-8").catch(async () => { }); if (AppMapper !== undefined) { const appManifest: any = []; const groupUids: any = groupByAppUid(JSON.parse(AppMapper)); diff --git a/api/src/services/migration.service.ts b/api/src/services/migration.service.ts index dc4348bb7..63efbb3c1 100644 --- a/api/src/services/migration.service.ts +++ b/api/src/services/migration.service.ts @@ -7,7 +7,7 @@ import { config } from '../config/index.js'; import { safePromise, getLogMessage } from '../utils/index.js'; import https from '../utils/https.utils.js'; import { LoginServiceType } from '../models/types.js'; -import getAuthtoken from '../utils/auth.utils.js'; +import getAuthtoken, { getAccessToken } from '../utils/auth.utils.js'; import logger from '../utils/logger.js'; import { HTTP_TEXTS, @@ -55,11 +55,21 @@ const createTestStack = async (req: Request): Promise => { const testStackName = `${name}-Test`; try { + let headers: any = { + organization_uid: orgId, + } + if(token_payload?.is_sso) { + const accessToken = await getAccessToken(token_payload?.region, token_payload?.user_id); + headers.authorization = `Bearer ${accessToken}`; + } else if (token_payload?.is_sso === false) { const authtoken = await getAuthtoken( token_payload?.region, token_payload?.user_id ); - + headers.authtoken = authtoken; + } else { + throw new BadRequestError("No valid authentication token found or mismatch in is_sso flag"); + } await ProjectModelLowdb.read(); const projectData: any = ProjectModelLowdb.chain .get('projects') @@ -77,10 +87,7 @@ const createTestStack = async (req: Request): Promise => { url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/stacks`, - headers: { - organization_uid: orgId, - authtoken, - }, + headers: headers, data: { stack: { name: newName, @@ -159,10 +166,21 @@ const deleteTestStack = async (req: Request): Promise => { const { token_payload, stack_key } = req.body; try { + let headers: any = { + api_key: stack_key, + } + if(token_payload?.is_sso) { + const accessToken = await getAccessToken(token_payload?.region, token_payload?.user_id); + headers.authorization = `Bearer ${accessToken}`; + } else if (token_payload?.is_sso === false) { const authtoken = await getAuthtoken( - token_payload?.region, - token_payload?.user_id - ); + token_payload?.region, + token_payload?.user_id + ); + headers.authtoken = authtoken; + } else { + throw new BadRequestError("No valid authentication token found or mismatch in is_sso flag"); + } const [err, res] = await safePromise( https({ @@ -170,10 +188,7 @@ const deleteTestStack = async (req: Request): Promise => { url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/stacks`, - headers: { - api_key: stack_key, - authtoken, - }, + headers: headers, }) ); diff --git a/api/src/services/org.service.ts b/api/src/services/org.service.ts index 72fa72eaf..39e40a557 100644 --- a/api/src/services/org.service.ts +++ b/api/src/services/org.service.ts @@ -3,7 +3,7 @@ import { config } from "../config/index.js"; import { safePromise, getLogMessage } from "../utils/index.js"; import https from "../utils/https.utils.js"; import { LoginServiceType } from "../models/types.js"; -import getAuthtoken from "../utils/auth.utils.js"; +import getAuthtoken, { getAccessToken } from "../utils/auth.utils.js"; import logger from "../utils/logger.js"; import { HTTP_TEXTS, HTTP_CODES } from "../constants/index.js"; import { ExceptionFunction } from "../utils/custom-errors.utils.js"; @@ -22,10 +22,22 @@ const getAllStacks = async (req: Request): Promise => { const search: string = req?.params?.searchText?.toLowerCase(); try { - const authtoken = await getAuthtoken( - token_payload?.region, - token_payload?.user_id - ); + let headers: any = { + organization_uid: orgId, + "Content-Type": "application/json", + }; + if (token_payload?.is_sso) { + const accessToken = await getAccessToken(token_payload?.region, token_payload?.user_id); + headers.authorization = `Bearer ${accessToken}`; + } else if (token_payload?.is_sso === false) { + const authtoken = await getAuthtoken( + token_payload?.region, + token_payload?.user_id + ); + headers.authtoken = authtoken; + } else { + throw new BadRequestError("No valid authentication token found or mismatch in is_sso flag"); + } const [err, res] = await safePromise( https({ @@ -33,10 +45,7 @@ const getAllStacks = async (req: Request): Promise => { url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/stacks`, - headers: { - organization_uid: orgId, - authtoken, - }, + headers: headers, }) ); if (err) { @@ -109,10 +118,22 @@ const createStack = async (req: Request): Promise => { const { token_payload, name, description, master_locale } = req.body; try { - const authtoken = await getAuthtoken( - token_payload?.region, - token_payload?.user_id - ); + let headers: any = { + organization_uid: orgId, + "Content-Type": "application/json", + }; + if (token_payload?.is_sso) { + const accessToken = await getAccessToken(token_payload?.region, token_payload?.user_id); + headers.authorization = `Bearer ${accessToken}`; + } else if (token_payload?.is_sso === false) { + const authtoken = await getAuthtoken( + token_payload?.region, + token_payload?.user_id + ); + headers.authtoken = authtoken; + } else { + throw new BadRequestError("No valid authentication token found or mismatch in is_sso flag"); + } const [err, res] = await safePromise( https({ @@ -120,10 +141,7 @@ const createStack = async (req: Request): Promise => { url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/stacks`, - headers: { - organization_uid: orgId, - authtoken, - }, + headers: headers, data: { stack: { name, @@ -182,10 +200,18 @@ const getLocales = async (req: Request): Promise => { const { token_payload } = req.body; try { - const authtoken = await getAuthtoken( - token_payload?.region, - token_payload?.user_id - ); + let headers: any = { + "Content-Type": "application/json", + }; + if (token_payload?.is_sso) { + const accessToken = await getAccessToken(token_payload?.region, token_payload?.user_id); + headers.authorization = `Bearer ${accessToken}`; + } else if (token_payload?.is_sso === false) { + const authtoken = await getAuthtoken(token_payload?.region, token_payload?.user_id); + headers.authtoken = authtoken; + } else { + throw new BadRequestError("No valid authentication token found or mismatch in is_sso flag"); + } const [err, res] = await safePromise( https({ @@ -193,9 +219,7 @@ const getLocales = async (req: Request): Promise => { url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/locales?include_all=true`, - headers: { - authtoken, - }, + headers: headers, }) ); @@ -242,10 +266,22 @@ const getStackStatus = async (req: Request) => { const { token_payload, stack_api_key } = req.body; const srcFunc = "getStackStatus"; - const authtoken = await getAuthtoken( + let headers: any = { + organization_uid: orgId, + "Content-Type": "application/json", + }; + if (token_payload?.is_sso) { + const accessToken = await getAccessToken(token_payload?.region, token_payload?.user_id); + headers.authorization = `Bearer ${accessToken}`; + } else if (token_payload?.is_sso === false) { + const authtoken = await getAuthtoken( token_payload?.region, token_payload?.user_id - ); + ); + headers.authtoken = authtoken; + } else { + throw new BadRequestError("No valid authentication token found or mismatch in is_sso flag"); + } try { const [stackErr, stackRes] = await safePromise( @@ -254,10 +290,7 @@ const getStackStatus = async (req: Request) => { url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/stacks`, - headers: { - organization_uid: orgId, - authtoken, - }, + headers: headers, }) ); @@ -282,10 +315,7 @@ const getStackStatus = async (req: Request) => { url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/content_types?skip=0&limit=1&include_count=true`, - headers: { - api_key: stack_api_key, - authtoken, - }, + headers: headers, }) ); @@ -330,10 +360,16 @@ const getStackLocale = async (req: Request) => { const { token_payload, stack_api_key } = req.body; const srcFunc = "getStackStatus"; - const authtoken = await getAuthtoken( - token_payload?.region, - token_payload?.user_id - ); + let headers: any = { + api_key: stack_api_key, + } + if(token_payload?.is_sso) { + const accessToken = await getAccessToken(token_payload?.region, token_payload?.user_id); + headers.authorization = `Bearer ${accessToken}`; + } else if (token_payload?.is_sso === false) { + const authtoken = await getAuthtoken(token_payload?.region, token_payload?.user_id); + headers.authtoken = authtoken; + } try { const [stackErr, stackRes] = await safePromise( @@ -342,10 +378,7 @@ const getStackLocale = async (req: Request) => { url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/locales`, - headers: { - api_key: stack_api_key, - authtoken, - }, + headers: headers, }) ); @@ -388,10 +421,15 @@ const getOrgDetails = async (req: Request) => { const { token_payload } = req.body; const srcFunc = "getOrgDetails"; - const authtoken = await getAuthtoken( - token_payload?.region, - token_payload?.user_id - ); + let headers: any = {} + if(token_payload?.is_sso) { + const accessToken = await getAccessToken(token_payload?.region, token_payload?.user_id); + headers.authorization = `Bearer ${accessToken}`; + } else if (token_payload?.is_sso === false) { + const authtoken = await getAuthtoken(token_payload?.region, token_payload?.user_id); + headers.authtoken = authtoken; + } + try { const [stackErr, stackRes] = await safePromise( @@ -400,9 +438,7 @@ const getOrgDetails = async (req: Request) => { url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/organizations/${orgId}?include_plan=true`, - headers: { - authtoken, - }, + headers: headers, }) ); diff --git a/api/src/services/projects.service.ts b/api/src/services/projects.service.ts index f2e6f7de1..fd602c9f4 100644 --- a/api/src/services/projects.service.ts +++ b/api/src/services/projects.service.ts @@ -15,13 +15,14 @@ import { HTTP_CODES, STEPPER_STEPS, NEW_PROJECT_STATUS, -} from '../constants/index.js'; -import { config } from '../config/index.js'; -import { getLogMessage, isEmpty, safePromise } from '../utils/index.js'; -import getAuthtoken from '../utils/auth.utils.js'; -import https from '../utils/https.utils.js'; -import getProjectUtil from '../utils/get-project.utils.js'; -import logger from '../utils/logger.js'; +} from "../constants/index.js"; +import { config } from "../config/index.js"; +import { getLogMessage, isEmpty, safePromise } from "../utils/index.js"; +import getAuthtoken, { getAccessToken } from "../utils/auth.utils.js"; +import https from "../utils/https.utils.js"; +import getProjectUtil from "../utils/get-project.utils.js"; +import logger from "../utils/logger.js"; +import AuthenticationModel from "../models/authentication.js"; // import { contentMapperService } from "./contentMapper.service.js"; import { v4 as uuidv4 } from 'uuid'; @@ -103,21 +104,23 @@ const getProject = async (req: Request) => { */ const createProject = async (req: Request) => { const orgId = req?.params?.orgId; - if (!orgId) { - throw new BadRequestError('Organization ID is required'); - } - - const { name, description } = req?.body || {}; - if (!name) { - throw new BadRequestError('Project name is required'); - } - - const decodedToken = req?.body?.token_payload; - if (!decodedToken) { - throw new BadRequestError('Token payload is required'); + const { name, description } = req.body; + const decodedToken = req.body.token_payload; + const { user_id = "", region = "" } = decodedToken; + let isSSO = false; + const srcFunc = "createProject"; + await AuthenticationModel.read(); + const userIndex = AuthenticationModel.chain + .get("users") + .findIndex({ + user_id: user_id, + region: region, + }) + .value(); + const userRecord = AuthenticationModel.data.users[userIndex]; + if(userRecord?.access_token){ + isSSO = true; } - const { user_id = '', region = '' } = decodedToken; - const srcFunc = 'createProject'; const projectData = { id: uuidv4(), region, @@ -156,8 +159,9 @@ const createProject = async (req: Request) => { }, mapperKeys: {}, isMigrationStarted: false, - isMigrationCompleted: false, - migration_execution: false, + isMigrationCompleted:false, + migration_execution:false, + isSSO: isSSO, }; try { @@ -761,15 +765,18 @@ const updateDestinationStack = async (req: Request) => { true )) as number; - const project = ProjectModelLowdb.data?.projects?.[projectIndex]; - if (!project) { - throw new NotFoundError(HTTP_TEXTS.PROJECT_NOT_FOUND); + const project = ProjectModelLowdb.data.projects[projectIndex]; + const headers :any = { + organization_uid: orgId, + } + if (project?.isSSO) { + const accessToken = await getAccessToken(token_payload?.region, token_payload?.user_id); + headers.authorization = `Bearer ${accessToken}`; + }else{ + headers.authtoken = await getAuthtoken(token_payload?.region, token_payload?.user_id); } - const authtoken = await getAuthtoken( - token_payload?.region, - token_payload?.user_id - ); + if ( project.status === NEW_PROJECT_STATUS[4] || @@ -802,10 +809,7 @@ const updateDestinationStack = async (req: Request) => { url: `${config.CS_API[ token_payload?.region as keyof typeof config.CS_API ]!}/stacks`, - headers: { - organization_uid: orgId, - authtoken, - }, + headers: headers, }) ); diff --git a/api/src/services/runCli.service.ts b/api/src/services/runCli.service.ts index 956e01834..8568d829e 100644 --- a/api/src/services/runCli.service.ts +++ b/api/src/services/runCli.service.ts @@ -16,7 +16,7 @@ interface TestStack { stackUid: string; isMigrated: boolean; } -import utilitiesHandler from '@contentstack/cli-utilities'; +import { setBasicAuthConfig, setOAuthConfig } from '../utils/config-handler.util.js'; /** * Determines log level based on message content without removing ANSI codes @@ -150,27 +150,29 @@ export const runCli = async ( const regionPresent = CS_REGIONS.find((item) => item === rg) ?? 'NA'.replace(/_/g, '-'); const regionCli = regionPresent.replace(/_/g, '-'); - // Fetch user authentication data await AuthenticationModel.read(); const userData = AuthenticationModel.chain .get('users') .find({ region: regionPresent, user_id }) .value(); - - // Configure CLI with region settings await runCommand( 'npx', ['@contentstack/cli', 'config:set:region', `${regionCli}`], transformePath ); // Pass the log file path here - // Set up authentication configuration for CLI - utilitiesHandler.configHandler.set('authtoken', userData.authtoken); - utilitiesHandler.configHandler.set('email', userData.email); - utilitiesHandler.configHandler.set('authorisationType', 'BASIC'); + if(userData?.access_token){ + setOAuthConfig(userData); + + }else if(userData?.authtoken){ + setBasicAuthConfig(userData); + }else { + throw new Error("No authentication token found"); + } + - if (userData?.authtoken && stack_uid) { + if (userData?.authtoken && stack_uid || userData?.access_token && stack_uid) { // Set up paths for backup and source data const { BACKUP_DATA, diff --git a/api/src/services/taxonomy.service.ts b/api/src/services/taxonomy.service.ts index c2215fde5..0b43f2bab 100644 --- a/api/src/services/taxonomy.service.ts +++ b/api/src/services/taxonomy.service.ts @@ -6,6 +6,7 @@ import fs from 'fs'; import { HTTP_TEXTS, MIGRATION_DATA_CONFIG } from "../constants/index.js"; import path from "path"; import logger from "../utils/logger.js"; +import AuthenticationModel from "../models/authentication.js"; const { TAXONOMIES_DIR_NAME, @@ -142,21 +143,35 @@ const createTaxonomy = async ({stackId,region,userId,current_test_stack_id} : const srcFun = "createTaxonomy"; const taxonomiesPath = path.join(MIGRATION_DATA_CONFIG.DATA, current_test_stack_id, TAXONOMIES_DIR_NAME); await fs.promises.mkdir(taxonomiesPath, { recursive: true }); + let headers: any = { + api_key : stackId, + } + let authtoken = ""; + await AuthenticationModel.read(); + const userIndex = AuthenticationModel.chain + .get('users') + .findIndex({ region, user_id: userId }) + .value(); + + const userData = AuthenticationModel?.data?.users[userIndex]; + if(userData?.access_token) { + + authtoken = `Bearer ${userData?.access_token}`; + headers.authorization = authtoken; + } else if(userData?.authtoken) { + authtoken = userData?.authtoken; + headers.authtoken = authtoken; + }else{ + throw new Error("No authentication token found"); + } try { - const authtoken = await getAuthtoken( - region, - userId - ); const [err, res] = await safePromise( https({ method: "GET", url: `${config.CS_API[ region as keyof typeof config.CS_API ]!}/taxonomies?include_terms_count=true&include_count=true`, - headers: { - api_key : stackId, - authtoken, - }, + headers: headers, }) ); if (err) { diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index 86f1d336c..53f1333ce 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -34,16 +34,25 @@ const getUserProfile = async (req: Request): Promise => { if (userIndex < 0) throw new BadRequestError(HTTP_TEXTS.NO_CS_USER); + const userRecord = AuthenticationModel?.data?.users[userIndex]; + let headers: any = { + "Content-Type": "application/json", + }; + if (appTokenPayload?.is_sso) { + headers.authorization = `Bearer ${userRecord?.access_token}`; + } else if (appTokenPayload?.is_sso === false) { + headers.authtoken = userRecord?.authtoken; + } else { + throw new BadRequestError("No valid authentication token found or mismatch in is_sso flag"); + } + const [err, res] = await safePromise( https({ method: "GET", url: `${config.CS_API[ appTokenPayload?.region as keyof typeof config.CS_API ]!}/user?include_orgs_roles=true`, - headers: { - "Content-Type": "application/json", - authtoken: AuthenticationModel.data.users[userIndex]?.authtoken, - }, + headers: headers, }) ); @@ -66,7 +75,7 @@ const getUserProfile = async (req: Request): Promise => { if (!res?.data?.user) throw new BadRequestError(HTTP_TEXTS.NO_CS_USER); const orgs = (res?.data?.user?.organizations || []) - ?.filter((org: any) => org?.org_roles?.some((item: any) => item.admin)) + ?.filter((org: any) => org?.org_roles?.some((item: any) => item?.admin)) ?.map(({ uid, name }: any) => ({ org_id: uid, org_name: name })); const ownerOrgs = (res?.data?.user?.organizations || [])?.filter((org:any)=> org?.is_owner) @@ -82,7 +91,7 @@ const getUserProfile = async (req: Request): Promise => { orgs: allOrgs, }, }, - status: res.status, + status: res?.status, }; } catch (error: any) { logger.error( diff --git a/api/src/utils/auth.utils.ts b/api/src/utils/auth.utils.ts index 30df20752..1d933ba0f 100644 --- a/api/src/utils/auth.utils.ts +++ b/api/src/utils/auth.utils.ts @@ -24,3 +24,21 @@ export default async (region: string, userId: string) => { return authToken; }; + + +export const getAccessToken = async (region: string, userId: string) => { + await AuthenticationModel.read(); + const userIndex = AuthenticationModel.chain + .get("users") + ?.findIndex({ + region: region, + user_id: userId, + }) + ?.value(); + + const accessToken = AuthenticationModel.data.users[userIndex]?.access_token; + + if (userIndex < 0 || !accessToken) throw new UnauthorizedError(); + + return accessToken; +}; \ No newline at end of file diff --git a/api/src/utils/config-handler.util.ts b/api/src/utils/config-handler.util.ts new file mode 100644 index 000000000..db789ebf0 --- /dev/null +++ b/api/src/utils/config-handler.util.ts @@ -0,0 +1,26 @@ +import { configHandler } from '@contentstack/cli-utilities'; + + +/** + * Sets the OAuth configuration for the CLI + * @param userData - The user data + */ +export const setOAuthConfig = (userData: any) => { + configHandler.set('oauthAccessToken', userData?.access_token); + configHandler.set('oauthRefreshToken', userData?.refresh_token); + configHandler.set('oauthDateTime', userData?.created_at); + configHandler.set('email', userData?.email); + configHandler.set('userUid', userData?.user_id); + configHandler.set('oauthOrgUid', userData?.organization_uid); + configHandler.set('authorisationType', 'OAUTH'); +} + +/** + * Sets the Basic Auth configuration for the CLI + * @param userData - The user data + */ +export const setBasicAuthConfig = (userData: any) => { + configHandler.set('authtoken', userData?.authtoken); + configHandler.set('email', userData?.email); + configHandler.set('authorisationType', 'BASIC'); +} \ No newline at end of file diff --git a/api/sso.utils.js b/api/sso.utils.js new file mode 100644 index 000000000..2a1c2b600 --- /dev/null +++ b/api/sso.utils.js @@ -0,0 +1,327 @@ +const contentstack = require("@contentstack/marketplace-sdk"); +const readline = require("readline"); +const { execSync } = require("child_process"); +const fs = require("fs"); +const crypto = require("crypto"); +const manifest = require("./manifest.json"); +const { default: axios } = require("axios"); + +// Region configuration +const REGION_CONFIG = { + NA: { + name: "North America", + cma: "https://api.contentstack.io", + cda: "https://cdn.contentstack.io", + app: "https://app.contentstack.com", + developerHub: "https://developerhub-api.contentstack.com", + personalize: "https://personalize-api.contentstack.com", + launch: "https://launch-api.contentstack.com", + }, + EU: { + name: "Europe", + cma: "https://eu-api.contentstack.com", + cda: "https://eu-cdn.contentstack.com", + app: "https://eu-app.contentstack.com", + developerHub: "https://eu-developerhub-api.contentstack.com", + personalize: "https://eu-personalize-api.contentstack.com", + launch: "https://eu-launch-api.contentstack.com", + }, + "AZURE-NA": { + name: "Azure North America", + cma: "https://azure-na-api.contentstack.com", + cda: "https://azure-na-cdn.contentstack.com", + app: "https://azure-na-app.contentstack.com", + developerHub: "https://azure-na-developerhub-api.contentstack.com", + personalize: "https://azure-na-personalize-api.contentstack.com", + launch: "https://azure-na-launch-api.contentstack.com", + }, + "AZURE-EU": { + name: "Azure Europe", + cma: "https://azure-eu-api.contentstack.com", + cda: "https://azure-eu-cdn.contentstack.com", + app: "https://azure-eu-app.contentstack.com", + developerHub: "https://azure-eu-developerhub-api.contentstack.com", + personalize: "https://azure-eu-personalize-api.contentstack.com", + launch: "https://azure-eu-launch-api.contentstack.com", + }, + "GCP-NA": { + name: "GCP North America", + cma: "https://gcp-na-api.contentstack.com", + cda: "https://gcp-na-cdn.contentstack.com", + app: "https://gcp-na-app.contentstack.com", + developerHub: "https://gcp-na-developerhub-api.contentstack.com", + personalize: "https://gcp-na-personalize-api.contentstack.com", + launch: "https://gcp-na-launch-api.contentstack.com", + }, +}; + + +/** + * Gets the current region from the CSDX config. + * @returns The current region. + */ +function getCurrentRegion() { + try { + const regionOutput = execSync("csdx config:get:region", { + encoding: "utf8", + }).trim(); + console.log("Raw region from CSDX config:", regionOutput); + + const regionMatch = regionOutput.match( + /\b(NA|EU|AZURE-NA|AZURE-EU|GCP-NA)\b/ + ); + + if (regionMatch) { + const regionKey = regionMatch[1]; + console.log("Extracted region key:", regionKey); + return regionKey; + } + + console.warn("Could not extract region from:", regionOutput); + return "NA"; + } catch (error) { + console.warn("Could not get region from CSDX:", error.message); + return "NA"; + } +} + +/** + * Sets the OAuth configuration for the CLI. + * @param migration - The migration object. + * @param stackSDKInstance - The stack SDK instance. + * @param managementAPIClient - The management API client. + */ +module.exports = async ({ + migration, + stackSDKInstance, + managementAPIClient, +}) => { + const axiosInstance = managementAPIClient.axiosInstance; + + + const regionKey = getCurrentRegion(); + const regionConfig = REGION_CONFIG[regionKey]; + + console.log(`\n=== USING REGION: ${regionConfig.name} (${regionKey}) ===`); + console.log(`CMA: ${regionConfig.cma}`); + console.log(`CDA: ${regionConfig.cda}`); + console.log(`App: ${regionConfig.app}`); + console.log("=".repeat(50)); + + try { + const user = await managementAPIClient.getUser(); + console.log(`✓ User: ${user.email} (${user.uid})`); + + if (!user.organizations || user.organizations.length === 0) { + console.log("❌ No organizations found"); + return; + } + + console.log(`\n=== YOUR ORGANIZATIONS ===`); + user.organizations.forEach((org, index) => { + console.log(`${index + 1}. ${org.name} (${org.uid})`); + }); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const selectedOrg = await new Promise((resolve) => { + rl.question(`\nSelect organization number: `, (answer) => { + rl.close(); + const index = parseInt(answer) - 1; + if (index >= 0 && index < user.organizations.length) { + resolve(user.organizations[index]); + } else { + console.log("❌ Invalid selection"); + resolve(null); + } + }); + }); + + if (!selectedOrg) { + console.log("❌ No organization selected. Exiting..."); + return; + } + + const headers = managementAPIClient.axiosInstance.defaults.headers; + const authtoken = headers.authtoken || headers.authorization; + + console.log(`\n✓ Selected: ${selectedOrg.name} (${selectedOrg.uid})`); + console.log( + `🔑 Auth token: ${ + authtoken ? authtoken.substring(0, 20) + "..." : "Not found" + }` + ); + + const orgDetails = await managementAPIClient + .organization(selectedOrg.uid) + .fetch(); + + console.log(`✓ Organization details fetched: ${orgDetails.name}`); + + const regionMapping = { + NA: "NA", + EU: "EU", + "AZURE-NA": "AZURE_NA", + "AZURE-EU": "AZURE_EU", + "GCP-NA": "GCP_NA", + "GCP-EU": "GCP_EU", + }; + + const sdkRegion = regionMapping[regionKey]; + + let clientConfig = { + authorization: authtoken, + }; + + if (regionKey !== "NA" && sdkRegion) { + clientConfig.region = contentstack.Region[sdkRegion]; + console.log(`✓ Setting SDK region to: ${sdkRegion}`); + } + + const client = contentstack.client(clientConfig); + + console.log(`✓ Contentstack client configured for ${regionKey} region`); + + // Find or create app + let existingApp = null; + + try { + console.log("🔍 Searching for existing app..."); + const allApps = await client.marketplace(selectedOrg.uid).findAllApps(); + existingApp = allApps?.items?.find((app) => app?.name === manifest?.name); + + if (!existingApp) { + console.log("📱 Creating new app..."); + existingApp = await client + .marketplace(selectedOrg.uid) + .app() + .create(manifest); + console.log(`✓ App created: ${existingApp.name} (${existingApp.uid})`); + } else { + console.log( + `✓ Found existing app: ${existingApp.name} (${existingApp.uid})` + ); + console.log("🔄 Updating existing app with manifest..."); + + // Update the existing app with the current manifest + const oauthUpdatePayload = { + redirect_uri: manifest?.oauth?.redirect_uri, + app_token_config: manifest?.oauth?.app_token_config || { + enabled: false, + scopes: [], + }, + user_token_config: manifest?.oauth?.user_token_config || { + enabled: true, + scopes: manifest?.oauth?.user_token_config?.scopes || [], + allow_pkce: true, + }, + }; + const updatedApp = await axios.put( + `${regionConfig.app}/apps-api/manifests/${existingApp?.uid}/oauth`, + oauthUpdatePayload, + { + headers: { + authorization: authtoken, + "Content-Type": "application/json", + organization_uid: selectedOrg.uid, + }, + } + ); + + console.log(`✓ App updated: ${existingApp.name} (${existingApp.uid})`); + } + } catch (error) { + console.error("❌ Error with app operations:", error.message); + if (error.status === 401) { + console.error(`\n💡 Authentication Error - This usually means:`); + console.error(` • Your auth token is from a different region`); + console.error( + ` • Please logout and login again in the ${regionKey} region` + ); + console.error(` • Commands: csdx auth:logout → csdx auth:login`); + } + throw error; + } + + console.log("🔐 Fetching OAuth configuration..."); + const oauthData = await client + ?.marketplace(selectedOrg?.uid) + ?.app(existingApp?.uid) + ?.oauth() + ?.fetch(); + + console.log("🔒 Generating PKCE credentials..."); + const code_verifier = crypto?.randomBytes(32).toString("hex"); + const code_challenge = crypto + ?.createHash("sha256") + ?.update(code_verifier) + ?.digest("base64") + ?.replace(/\+/g, "-") + ?.replace(/\//g, "_") + ?.replace(/=+$/, ""); + + // Generates the authorization URL for the app + const authUrl = `${regionConfig.app}/#!/apps/${ + existingApp?.uid + }/authorize?response_type=code&client_id=${ + oauthData?.client_id + }&redirect_uri=${encodeURIComponent( + oauthData?.redirect_uri + )}&code_challenge=${code_challenge}&code_challenge_method=S256`; + + console.log(`\n🚀 Authorization URL for ${regionConfig.name}:`); + console.log(authUrl); + + // Formats the app data for the app.json file + const appData = { + timestamp: new Date().toISOString(), + region: { + key: regionKey, + name: regionConfig.name, + endpoints: regionConfig, + }, + user: { + email: user.email, + uid: user.uid, + }, + organization: { + name: selectedOrg.name, + uid: selectedOrg.uid, + }, + app: { + name: existingApp?.name, + uid: existingApp?.uid, + manifest: manifest.name, + }, + oauthData: oauthData, + pkce: { + code_verifier: code_verifier, + code_challenge: code_challenge, + }, + authUrl: authUrl, + }; + + fs.writeFileSync("app.json", JSON.stringify(appData, null, 2)); + console.log("✓ OAuth data & Auth URL logged to app.json"); + + } catch (error) { + console.error("❌ Setup failed:"); + console.error("Error:", error?.message); + + if (error?.errorMessage) { + console.error("Details:", error?.errorMessage); + } + + console.error(`\n🔍 Debug Info:`); + console.error(`Region: ${regionKey} (${regionConfig?.name || "Unknown"})`); + console.error(`Expected CMA: ${regionConfig?.cma || "Unknown"}`); + console.error( + `Management API URL: ${managementAPIClient.axiosInstance.defaults.baseURL}` + ); + + throw error; + } +}; \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..4c2639826 --- /dev/null +++ b/build.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +# --- Function to get current region --- +get_current_region() { + local region=$(csdx config:get:region 2>/dev/null) + if [ $? -eq 0 ] && [ -n "$region" ]; then + echo "$region" + return 0 + else + echo "Not set" + return 1 + fi +} + +# --- Prompt for Region --- +echo "" +echo "Please select your region:" +echo "1. NA (North America)" +echo "2. EU (Europe)" +echo "3. AZURE-NA (Azure North America)" +echo "4. AZURE-EU (Azure Europe)" +echo "5. GCP-NA (GCP North America)" +read -p "Enter region number (default: 1): " REGION_CHOICE + +case $REGION_CHOICE in + 2) REGION="EU";; + 3) REGION="AZURE-NA";; + 4) REGION="AZURE-EU";; + 5) REGION="GCP-NA";; + *) REGION="NA";; +esac + +echo "Selected region: $REGION" + +# --- Set the Region in CSDX Config --- +echo "" +echo "Setting the region in CSDX..." +if ! csdx config:set:region "$REGION"; then + echo "Failed to set the region. Please check your CSDX installation." + exit 1 +fi +echo "✓ Region set to $REGION." + +# --- Get and Verify the Region --- +echo "" +echo "Verifying the region configuration..." +CURRENT_REGION=$(csdx config:get:region) +if [ $? -eq 0 ]; then + echo "✓ Current region is set to: $CURRENT_REGION" +else + echo "⚠ Could not retrieve current region configuration" +fi + +# --- OAuth Login (Always redirect after region selection) --- +echo "" +echo "Redirecting to OAuth login..." +echo "This will open your browser for authentication in the selected region ($REGION)." +if ! csdx auth:login --oauth; then + echo "OAuth login failed. Please try again." + exit 1 +fi +echo "✓ OAuth login successful for region: $REGION" + +# Update redirect_uri in manifest.json +JSON_FILE="api/manifest.json" +if [ -f "$JSON_FILE" ]; then + echo "" + read -p "Enter new redirect_uri or press enter to use default value: " NEW_URI + + #default value + if [ -z "$NEW_URI" ]; then + NEW_URI="http://localhost:5001" + fi + + sed -i '' "s|\"redirect_uri\"[[:space:]]*:[[:space:]]*\"[^\"]*\"|\"redirect_uri\": \"${NEW_URI}/v2/auth/save-token\"|g" "$JSON_FILE" + echo "✓ redirect_uri updated to ${NEW_URI}/v2/auth/save-token in $JSON_FILE" +else + echo "⚠ manifest.json file not found at: $JSON_FILE" +fi + +# Run the Migration Script +echo "" +echo "Running the migration..." +SCRIPT_PATH="api/sso.utils.js" +if [ -f "$SCRIPT_PATH" ]; then + csdx cm:stacks:migration --file-path "$SCRIPT_PATH" +else + echo "Migration script not found at: $SCRIPT_PATH" + echo "Please update the script path in build.sh" + exit 1 +fi + +echo "" +echo "✓ Setup script finished." \ No newline at end of file diff --git a/ui/src/pages/Login/index.scss b/ui/src/pages/Login/index.scss index 82b05fb7e..c5f5f5dc8 100644 --- a/ui/src/pages/Login/index.scss +++ b/ui/src/pages/Login/index.scss @@ -187,3 +187,49 @@ line-height: 1 !important; } } +// SSO Button specific styles +.AccountForm__actions__sso_button { + margin-bottom: 24px; +} + +// Divider styles +.flex { + display: flex; +} + +.items-center { + align-items: center; +} + +.flex-1 { + flex: 1; +} + +.border-t { + border-top: 1px solid; +} + +.border-gray-300 { + border-color: #d1d5db; +} + +.px-16 { + padding-left: 16px; + padding-right: 16px; +} + +.text-sm { + font-size: 14px; +} + +.text-gray-500 { + color: #6b7280; +} + +.bg-white { + background-color: white; +} + +.mb-24 { + margin-bottom: 24px; +} \ No newline at end of file diff --git a/ui/src/pages/Login/index.tsx b/ui/src/pages/Login/index.tsx index bbfde4a23..4ecaeb2aa 100644 --- a/ui/src/pages/Login/index.tsx +++ b/ui/src/pages/Login/index.tsx @@ -26,7 +26,7 @@ import { clearLocalStorage, failtureNotification, setDataInLocalStorage } from ' // API Service import { getCMSDataFromFile } from '../../cmsData/cmsSelector'; -import { userSession, requestSMSToken } from '../../services/api/login.service'; +import { userSession, requestSMSToken, getAppConfig, checkSSOAuthStatus } from '../../services/api/login.service'; // Interface import { IProps, IStates, defaultStates, User, UserRes, LoginType } from './login.interface'; @@ -238,6 +238,170 @@ const Login: FC = () => { }; }; + const handleSSOLogin = async () => { + setIsLoading(true); + try { + const currentRegion = region; + + await getAppConfig() + .then((res: any) => { + if (res?.status === 404) { + failtureNotification('Kindly setup the SSO first'); + setIsLoading(false); + return; + } + + if (res?.status === 400) { + failtureNotification('Something went wrong please try normal login method'); + setIsLoading(false); + return; + } + + if (res?.status === 500) { + failtureNotification('Something went wrong please try normal login method'); + setIsLoading(false); + return; + } + + const appConfig = res?.data; + + // Check if authUrl exists + if (!appConfig?.authUrl) { + failtureNotification('Something went wrong please try normal login method'); + setIsLoading(false); + return; + } + + // Checks if region matches + if (appConfig?.region?.key && appConfig?.region?.key !== currentRegion) { + failtureNotification('Kindly choose correct region as the SSO region'); + setIsLoading(false); + return; + } + + const authURL = appConfig?.authUrl; + const ssoWindow = window.open(authURL, '_blank', 'noopener,noreferrer'); + + if (appConfig?.user?.uid) { + startSSOPolling(appConfig?.user?.uid, ssoWindow); + } else { + failtureNotification('Missing user information in SSO configuration'); + setIsLoading(false); + } + + }) + .catch((err: any) => { + failtureNotification('Something went wrong please try normal login method'); + setIsLoading(false); + }); + + } catch (error) { + failtureNotification('Something went wrong please try normal login method'); + setIsLoading(false); + } + }; + + + const startSSOPolling = (userId: string, ssoWindow: Window | null) => { + const pollInterval = 2000; + const maxPollTime = 300000; + let pollCount = 0; + const maxPolls = maxPollTime / pollInterval; + const poll = async () => { + pollCount++; + + try { + if (ssoWindow?.closed) { + failtureNotification('SSO login was cancelled'); + setIsLoading(false); + return; + } + + await checkSSOAuthStatus(userId) + .then((authRes: any) => { + + if (authRes?.status === 200 && authRes?.data?.authenticated === true) { + + if (ssoWindow && !ssoWindow.closed) { + ssoWindow.close(); + } + + handleSuccessfulSSOLogin(authRes?.data); + return; + } + + if (pollCount < maxPolls) { + setTimeout(poll, pollInterval); + } else { + failtureNotification('SSO authentication timed out. Please try again.'); + setIsLoading(false); + if (ssoWindow && !ssoWindow.closed) { + ssoWindow.close(); + } + } + }) + .catch((error: any) => { + + + if (pollCount < maxPolls) { + setTimeout(poll, pollInterval); + } else { + failtureNotification('Something went wrong please try normal login method'); + setIsLoading(false); + if (ssoWindow && !ssoWindow.closed) { + ssoWindow.close(); + } + } + }); + + } catch (error) { + failtureNotification('Something went wrong please try normal login method'); + setIsLoading(false); + if (ssoWindow && !ssoWindow.closed) { + ssoWindow.close(); + } + } + }; + + setTimeout(poll, pollInterval); + }; + + + const handleSuccessfulSSOLogin = (authData: any) => { + + try { + setIsLoading(false); + + setDataInLocalStorage('app_token', authData?.app_token); + + localStorage?.removeItem('organization'); + dispatch(clearOrganisationData()); + + + const authenticationObj = { + authToken: authData?.app_token, + isAuthenticated: true + }; + + const userObj = { + ...user, + region: region + }; + + dispatch(setUser(userObj)); + dispatch(setAuthToken(authenticationObj)); + setLoginStates((prev) => ({ ...prev, submitted: true })); + + dispatch(getUserDetails()); + + navigate(`/projects`, { replace: true }); + + } catch (error) { + console.error('Error processing SSO login success:', error); + failtureNotification('Login successful but navigation failed. Please refresh the page.'); + } + }; + // useEffect(()=>{ // const handlePopState = (event: PopStateEvent) => { // event.preventDefault(); @@ -281,7 +445,7 @@ const Login: FC = () => { {twoFactorAuthentication?.title && (

{twoFactorAuthentication?.title}

)} - + ( @@ -461,10 +625,9 @@ const Login: FC = () => { }} - +
- {/* disabled={errors && Object.keys(errors).length ? true : false} */}
+
+ +
diff --git a/ui/src/services/api/login.service.ts b/ui/src/services/api/login.service.ts index acf3300a3..fe9be1a20 100644 --- a/ui/src/services/api/login.service.ts +++ b/ui/src/services/api/login.service.ts @@ -1,7 +1,7 @@ import { AUTH_ROUTES } from '../../utilities/constants'; import { User, SmsToken } from '../../pages/Login/login.interface'; -import { postCall } from './service'; +import { postCall, getCall } from './service'; export const userSession = (data: User) => { try { @@ -26,3 +26,27 @@ export const requestSMSToken = (data: SmsToken) => { } } }; + +export const getAppConfig = () => { + try { + return getCall(`${AUTH_ROUTES}/app-config`); + } catch (error) { + if (error instanceof Error) { + throw new Error(`Error in getAppConfig: ${error.message}`); + } else { + throw new Error('Unknown error in getAppConfig'); + } + } +}; + +export const checkSSOAuthStatus = (userId: string) => { + try { + return getCall(`${AUTH_ROUTES}/sso-status/${userId}`); + } catch (error) { + if (error instanceof Error) { + throw new Error(`Error in checkSSOAuthStatus: ${error.message}`); + } else { + throw new Error('Unknown error in checkSSOAuthStatus'); + } + } +}; \ No newline at end of file diff --git a/upload-api/src/config/index.ts b/upload-api/src/config/index.ts index 9bd6fb1ae..9ea8498b3 100644 --- a/upload-api/src/config/index.ts +++ b/upload-api/src/config/index.ts @@ -2,7 +2,7 @@ export default { plan: { dropdown: { optionLimit: 100 } }, - cmsType: process.env.CMS_TYPE || 'cmsType', + cmsType: process.env.CMS_TYPE || 'wordpress', isLocalPath: true, awsData: { awsRegion: 'us-east-2', @@ -12,5 +12,5 @@ export default { bucketName: '', bucketKey: '' }, - localPath: process.env.CONTAINER_PATH || 'localPath' + localPath: process.env.CONTAINER_PATH || 'your-local-legacy-cms-path', };