Skip to content

Commit

Permalink
feat(cli-logger): enhance login, storing file in XDG file path
Browse files Browse the repository at this point in the history
Create the user if it doesn't exist
Update logs
Use spinner for authentication
  • Loading branch information
nathan-pichon committed Apr 23, 2024
1 parent cbf0d32 commit b544834
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 46 deletions.
1 change: 1 addition & 0 deletions packages/core/strapi/package.json
Expand Up @@ -181,6 +181,7 @@
"statuses": "2.0.1",
"tar": "6.1.13",
"typescript": "5.2.2",
"xdg-app-paths": "8.3.0",
"yalc": "1.0.0-pre.53",
"yup": "0.32.9"
},
Expand Down
54 changes: 49 additions & 5 deletions packages/core/strapi/src/commands/actions/cloud/config/local.ts
@@ -1,15 +1,59 @@
import path from 'path';
import os from 'os';
import fs from 'fs';
import XDGAppPaths from 'xdg-app-paths';

export const TMP_DIR = path.join(os.tmpdir(), 'strapi-cli');
export const TMP_TOKEN_FILE = 'strapi.cloud';
const APP_FOLDER_NAME = 'com.strapi.cli';

export const CONFIG_FILENAME = 'config.json';

export type LocalConfig = {
token?: string;
};

function checkDirectoryExists(directoryPath: string) {
try {
return fs.lstatSync(directoryPath).isDirectory();
} catch (e) {
return false;
}
}

// Determine storage path based on the operating system
export function getStoragePath() {
const storagePath = TMP_DIR;
if (!fs.existsSync(storagePath)) {
export function getTmpStoragePath() {
const storagePath = path.join(os.tmpdir(), APP_FOLDER_NAME);
if (!checkDirectoryExists(storagePath)) {
fs.mkdirSync(storagePath, { recursive: true });
}
return storagePath;
}

function getConfigPath() {
const configDirs = XDGAppPaths(APP_FOLDER_NAME).configDirs();
const configPath = configDirs.find(checkDirectoryExists);

if (!configPath) {
fs.mkdirSync(configDirs[0], { recursive: true });
return configDirs[0];
}
return configPath;
}

export function getLocalConfig(): LocalConfig {
const configPath = getConfigPath();
const configFilePath = path.join(configPath, CONFIG_FILENAME);
if (!fs.existsSync(configFilePath)) {
return {};
}
try {
return JSON.parse(fs.readFileSync(configFilePath, 'utf8'));
} catch (e) {
return {};
}
}

export function saveLocalConfig(data: LocalConfig) {
const configPath = getConfigPath();
const configFilePath = path.join(configPath, CONFIG_FILENAME);
fs.writeFileSync(configFilePath, JSON.stringify(data), { encoding: 'utf8', mode: 0o600 });
}
Expand Up @@ -7,7 +7,7 @@ import { cloudApiFactory, type ProjectInfos } from '../services/cli-api';
import * as localSave from '../services/strapi-info-save';
import createProjectAction from '../create-project/action';
import { type CLIContext } from '../../../types';
import { getStoragePath } from '../config/local';
import { getTmpStoragePath } from '../config/local';
import { tokenServiceFactory } from '../utils/token';
import { notificationServiceFactory } from '../utils/notification-service';
import { loadPkg } from '../../../utils/pkg';
Expand All @@ -25,7 +25,7 @@ async function upload(ctx: CLIContext, project: ProjectInfos, token: string) {
const cloudApi = cloudApiFactory(token);
// * Upload project
try {
const storagePath = getStoragePath();
const storagePath = getTmpStoragePath();
const projectFolder = path.resolve(process.cwd());
const packageJson = (await loadPkg(ctx)) as PackageJson;

Expand Down
35 changes: 29 additions & 6 deletions packages/core/strapi/src/commands/actions/cloud/login/action.ts
Expand Up @@ -23,6 +23,11 @@ export default async (ctx: CLIContext) => {
return;
}

logger.debug('🔐 Creating device authentication request...', {
client_id: cliConfig.clientId,
scope: cliConfig.scope,
audience: cliConfig.audience,
});
const deviceAuthResponse = (await axios
.post(cliConfig.deviceCodeAuthUrl, {
client_id: cliConfig.clientId,
Expand Down Expand Up @@ -50,7 +55,7 @@ export default async (ctx: CLIContext) => {
`1. Open this url in your device: ${deviceAuthResponse.data.verification_uri_complete}`
);
logger.log(
`2. Enter the following code: ${deviceAuthResponse.data.user_code} and confirm to login.`
`2. Enter the following code: ${deviceAuthResponse.data.user_code} and confirm to login.\n`
);

const tokenPayload = {
Expand All @@ -62,44 +67,60 @@ export default async (ctx: CLIContext) => {
let isAuthenticated = false;

const authenticate = async () => {
const spinner = logger.spinner('Waiting for authentication');
spinner.start();
const spinnerFail = () => spinner.fail('Authentication failed!');
while (!isAuthenticated) {
logger.log(
`⏳ Checking if the authentication process is complete... retrying again in ${deviceAuthResponse.data.interval} seconds.`
);
try {
const tokenResponse = await axios.post(cliConfig.tokenUrl, tokenPayload);
const authTokenData = tokenResponse.data;

if (tokenResponse.status === 200) {
// Token validation
try {
logger.debug('🔐 Validating token...');
await tokenService.validateToken(authTokenData.id_token, cliConfig.jwksUrl);
logger.debug('🔐 Token validation successful!');
} catch (error: any) {
logger.debug(error);
spinnerFail();
throw new Error('Unable to proceed: Token validation failed');
}
isAuthenticated = true;
logger.success('🚀 Authentication successful!');
const cloudApiService = cloudApiFactory(authTokenData.access_token);

logger.debug('🔍 Fetching user information...');
// Call to get user info to create the user in DB if not exists
await cloudApiService.getUserInfo();
logger.debug('🔍 User information fetched successfully!');

try {
logger.debug('📝 Saving login information...');
await tokenService.saveToken(authTokenData.access_token);
logger.debug('📝 Login information saved successfully!');
isAuthenticated = true;
} catch (error) {
logger.error(
'There was a problem saving your login information. Please try logging in again.'
);
logger.debug(error);
spinnerFail();
return;
}
}
} catch (error: any) {
if (error.message === 'Unable to proceed: Token validation failed') {
logger.error(
'There seems to be a problem with your login information. Please try logging in again.'
);
spinnerFail();
return;
}
if (
error.response?.data.error &&
!['authorization_pending', 'slow_down'].includes(error!.response.data.error)
) {
logger.debug(error);
spinnerFail();
return;
}
// Await interval before retrying
Expand All @@ -108,6 +129,8 @@ export default async (ctx: CLIContext) => {
});
}
}
spinner.succeed('Authentication successful!');
logger.log('You are now logged in to Strapi Cloud.');
};

await authenticate();
Expand Down
Expand Up @@ -68,6 +68,10 @@ export function cloudApiFactory(token?: string) {
});
},

getUserInfo() {
return axiosCloudAPI.get('/user');
},

config(): Promise<AxiosResponse<CloudCliConfig>> {
return axiosCloudAPI.get('/config');
},
Expand Down
49 changes: 28 additions & 21 deletions packages/core/strapi/src/commands/actions/cloud/utils/token.ts
@@ -1,9 +1,7 @@
import jwksClient, { type JwksClient, type SigningKey } from 'jwks-rsa';
import type { JwtHeader, VerifyErrors } from 'jsonwebtoken';
import jwt from 'jsonwebtoken';
import fs from 'fs';
import path from 'path';
import { getStoragePath, TMP_TOKEN_FILE } from '../config/local';
import { getLocalConfig, saveLocalConfig } from '../config/local';
import type { CloudCliConfig } from '../types';
import { cloudApiFactory } from '../services/cli-api';
import type { CLIContext } from '../../../types';
Expand All @@ -18,25 +16,32 @@ interface DecodedToken {

export function tokenServiceFactory({ logger }: CLIContext) {
function saveToken(str: string) {
const storagePath = getStoragePath();
const filePath = path.join(storagePath, TMP_TOKEN_FILE);
const appConfig = getLocalConfig();

if (!appConfig) {
logger.error('There was a problem saving your token. Please try again.');
return;
}

appConfig.token = str;

try {
fs.writeFileSync(filePath, str);
saveLocalConfig(appConfig);
} catch (error: Error | unknown) {
logger.debug(error);
logger.error('There was a problem saving your token. Please try again.');
}
}

async function retrieveToken() {
const storagePath = getStoragePath();
const filePath = path.join(storagePath, TMP_TOKEN_FILE);

try {
return fs.readFileSync(filePath, 'utf8');
} catch (error) {
return null;
const appConfig = getLocalConfig();
if (appConfig.token) {
// check if token is still valid
if (await isTokenValid(appConfig.token)) {
return appConfig.token;
}
}
return null;
}

async function validateToken(idToken: string, jwksUrl: string): Promise<void> {
Expand Down Expand Up @@ -99,18 +104,20 @@ export function tokenServiceFactory({ logger }: CLIContext) {
}

function eraseToken() {
const storagePath = getStoragePath();
const filePath = path.join(storagePath, TMP_TOKEN_FILE);
const appConfig = getLocalConfig();
if (!appConfig) {
return;
}

delete appConfig.token;

try {
// if path doesn't exist we consider it logged out
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (error) {
saveLocalConfig(appConfig);
} catch (error: Error | unknown) {
logger.debug(error);
logger.error(
'There was an issue removing your login information. Please try logging out again.'
);
logger.debug(error);
}
}

Expand Down
63 changes: 51 additions & 12 deletions yarn.lock
Expand Up @@ -8854,6 +8854,7 @@ __metadata:
tar: "npm:6.1.13"
tsconfig: "npm:4.23.0"
typescript: "npm:5.2.2"
xdg-app-paths: "npm:8.3.0"
yalc: "npm:1.0.0-pre.53"
yup: "npm:0.32.9"
bin:
Expand Down Expand Up @@ -17763,6 +17764,16 @@ __metadata:
languageName: node
linkType: hard

"fsevents@npm:*, fsevents@npm:~2.3.3":
version: 2.3.3
resolution: "fsevents@npm:2.3.3"
dependencies:
node-gyp: "npm:latest"
checksum: 4c1ade961ded57cdbfbb5cac5106ec17bc8bccd62e16343c569a0ceeca83b9dfef87550b4dc5cbb89642da412b20c5071f304c8c464b80415446e8e155a038c0
conditions: os=darwin
languageName: node
linkType: hard

"fsevents@npm:2.3.2, fsevents@npm:^2.3.2, fsevents@npm:~2.3.2":
version: 2.3.2
resolution: "fsevents@npm:2.3.2"
Expand All @@ -17773,12 +17784,11 @@ __metadata:
languageName: node
linkType: hard

"fsevents@npm:~2.3.3":
"fsevents@patch:fsevents@npm%3A*#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
version: 2.3.3
resolution: "fsevents@npm:2.3.3"
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
dependencies:
node-gyp: "npm:latest"
checksum: 4c1ade961ded57cdbfbb5cac5106ec17bc8bccd62e16343c569a0ceeca83b9dfef87550b4dc5cbb89642da412b20c5071f304c8c464b80415446e8e155a038c0
conditions: os=darwin
languageName: node
linkType: hard
Expand All @@ -17792,15 +17802,6 @@ __metadata:
languageName: node
linkType: hard

"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
version: 2.3.3
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
dependencies:
node-gyp: "npm:latest"
conditions: os=darwin
languageName: node
linkType: hard

"function-bind@npm:^1.1.1":
version: 1.1.1
resolution: "function-bind@npm:1.1.1"
Expand Down Expand Up @@ -24898,6 +24899,18 @@ __metadata:
languageName: node
linkType: hard

"os-paths@npm:^7.4.0":
version: 7.4.0
resolution: "os-paths@npm:7.4.0"
dependencies:
fsevents: "npm:*"
dependenciesMeta:
fsevents:
optional: true
checksum: dc362e76cbe20fa39a19005df7bc8b68a7ccf145b64d2f5aa976ee09e2f8a355d811954df7ffc9668ff083dcddffd23bb0de33e968d49eca15c234bff2fd33d7
languageName: node
linkType: hard

"os-tmpdir@npm:~1.0.2":
version: 1.0.2
resolution: "os-tmpdir@npm:1.0.2"
Expand Down Expand Up @@ -31546,13 +31559,39 @@ __metadata:
languageName: node
linkType: hard

"xdg-app-paths@npm:8.3.0":
version: 8.3.0
resolution: "xdg-app-paths@npm:8.3.0"
dependencies:
fsevents: "npm:*"
xdg-portable: "npm:^10.6.0"
dependenciesMeta:
fsevents:
optional: true
checksum: 9309fde27cf175d52ed119f53dbd6a2c7afde5f2b7170d381c579f10536e7101ea8076bd5da627209c7b6ba23c73386da7d8f26055400ce4debb284d636f9f9d
languageName: node
linkType: hard

"xdg-basedir@npm:^4.0.0":
version: 4.0.0
resolution: "xdg-basedir@npm:4.0.0"
checksum: 0073d5b59a37224ed3a5ac0dd2ec1d36f09c49f0afd769008a6e9cd3cd666bd6317bd1c7ce2eab47e1de285a286bad11a9b038196413cd753b79770361855f3c
languageName: node
linkType: hard

"xdg-portable@npm:^10.6.0":
version: 10.6.0
resolution: "xdg-portable@npm:10.6.0"
dependencies:
fsevents: "npm:*"
os-paths: "npm:^7.4.0"
dependenciesMeta:
fsevents:
optional: true
checksum: c73463ff4329de4874322dd843d70ba12311de02bcc65cae6cbd58a24c56ad402e15561b5a7ee0aa6ba26bb0ec440d316adc475abee1bbdf82755f3588fc83f1
languageName: node
linkType: hard

"xml-name-validator@npm:^4.0.0":
version: 4.0.0
resolution: "xml-name-validator@npm:4.0.0"
Expand Down

0 comments on commit b544834

Please sign in to comment.