All files / src/lib profile.js

45.52% Statements 56/123
80% Branches 4/5
33.33% Functions 1/3
45.52% Lines 56/123

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 18x 18x 18x 18x 18x 18x 18x 18x 193x 193x 193x 193x 193x 193x 193x 193x 193x                                                                       193x 193x 193x 193x 193x 193x                                                                  
import { get as getUser } from '@clevercloud/client/esm/api/v2/organisation.js';
import { getCurrentTokenInfo } from '@clevercloud/client/esm/api/v2/self.js';
import { baseConfig } from '../config/config.js';
import { sendToApiWithConfig } from '../models/send-to-api.js';
import { formatDateLocalized, toDate } from './date-utils.js';
import { styleText } from './style-text.js';
 
/**
 * @typedef {import('../config/config.js').Profile} Profile
 */
 
/**
 * @typedef {object} ProfileDetails
 * @property {string} alias
 * @property {string} email
 * @property {string | null | undefined} name
 * @property {string} [id]
 * @property {Date} [tokenExpiration]
 * @property {boolean} has2FA
 * @property {string} [avatar]
 * @property {Date} [creationDate]
 * @property {string} [lang]
 * @property {boolean} isProfileActive
 * @property {boolean} isTokenValid
 * @property {Record<string, string>} [overrides]
 */
 
/**
 * Formats a profile for display.
 * @param {Profile | ProfileDetails} profile
 * @returns {string}
 */
export function formatProfile(profile) {
  return [
    profile.alias,
    profile.email != null ? `(${profile.email})` : null,
    profile.isProfileActive ? styleText('green', '[active]') : null,
  ]
    .filter(Boolean)
    .join(' ');
}
 
/**
 * Fetch full profile details using the profile's credentials.
 * @param {object} params
 * @param {Profile} params.profile
 * @param {boolean} params.isActive
 * @returns {Promise<ProfileDetails>}
 */
export async function getProfileDetails({ profile, isActive }) {
  const sendWithCredentials = sendToApiWithConfig({
    token: profile.token,
    secret: profile.secret,
    apiHost: profile.overrides?.API_HOST ?? baseConfig.API_HOST,
    consumerKey: profile.overrides?.OAUTH_CONSUMER_KEY ?? baseConfig.OAUTH_CONSUMER_KEY,
    consumerSecret: profile.overrides?.OAUTH_CONSUMER_SECRET ?? baseConfig.OAUTH_CONSUMER_SECRET,
  });

  const [user, token] = await Promise.all([
    getUser({}).then(sendWithCredentials),
    getCurrentTokenInfo().then(sendWithCredentials),
  ]).catch((error) => {
    // An expired/invalid token surfaces as a 401: degrade gracefully so the command can report it.
    // Any other failure (TLS, network…) must bubble up instead of being masked as "token invalid".
    if (error?.cause?.response?.status === 401) {
      return [null, null];
    }
    throw error;
  });

  return {
    id: user?.id ?? profile.userId,
    email: user?.email ?? profile.email,
    name: user?.name,
    avatar: user?.avatar,
    creationDate: toDate(user?.creationDate),
    tokenExpiration: toDate(token?.expirationDate ?? profile.expirationDate),
    lang: user?.lang,
    has2FA: user != null ? user.preferredMFA != null && user.preferredMFA !== 'NONE' : undefined,
    alias: profile.alias,
    isProfileActive: isActive,
    isTokenValid: user != null,
    overrides: profile.overrides,
  };
}
 
/**
 * Formats profile details for CLI display.
 * @param {ProfileDetails} profile
 */
export function formatProfileDetails(profile) {
  const lines = [];

  lines.push(styleText('bold', formatProfile(profile)));
  lines.push(profile.id);

  if (!profile.isTokenValid) {
    lines.push(styleText('red', 'Invalid or expired token'));
    return lines.map((line, index) => (index === 0 ? line : `  ${line}`)).join('\n');
  }

  lines.push(profile.name ? profile.name : '[unknown]');

  const expiresAtFormatted = formatDateLocalized(profile.tokenExpiration);
  if (expiresAtFormatted) {
    lines.push(`Expires on ${styleText('gray', expiresAtFormatted)}`);
  }

  lines.push(`2FA ${profile.has2FA ? styleText('green', 'enabled ✓') : styleText('red', 'disabled ✗')}`);

  if (profile.overrides != null) {
    const overrideEntries = Object.entries(profile.overrides).filter(([, v]) => v != null);
    for (const [key, value] of overrideEntries) {
      lines.push(`${key}: ${styleText('gray', value)}`);
    }
  }

  return lines
    .map((line, index) => {
      return index === 0 ? line : `  ${line}`;
    })
    .join('\n');
}