Skip to content

Commit

Permalink
Merge branch 'next' into bugfix/replace-netrc-with-json
Browse files Browse the repository at this point in the history
  • Loading branch information
alvarosabu committed Jan 14, 2025
2 parents 445f8ab + bcbb136 commit 27f97da
Show file tree
Hide file tree
Showing 16 changed files with 356 additions and 80 deletions.
28 changes: 28 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@
"STUB": "true"
}
},
{
"type": "node",
"request": "launch",
"name": "Debug login by token",
"program": "${workspaceFolder}/dist/index.mjs",
"args": ["login", "--token", "1234567890"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"env": {
"STUB": "true"
}
},
{
"type": "node",
"request": "launch",
Expand Down Expand Up @@ -68,6 +82,20 @@
"env": {
"STUB": "true"
}
},
{
"type": "node",
"request": "launch",
"name": "Debug Test",
"program": "${workspaceFolder}/dist/index.mjs",
"args": ["test"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"env": {
"STUB": "true"
}
}
]
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"chalk": "^5.3.0",
"commander": "^12.1.0",
"dotenv": "^16.4.5",
"ofetch": "^1.4.0",
"storyblok-js-client": "^6.9.2"
},
"devDependencies": {
Expand Down
22 changes: 0 additions & 22 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 15 additions & 11 deletions src/commands/login/actions.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import chalk from 'chalk'
import type { RegionCode } from '../../constants'
import { regionsDomain } from '../../constants'
import { FetchError, ofetch } from 'ofetch'
import { customFetch, FetchError } from '../../utils/fetch'
import { APIError, handleAPIError, maskToken } from '../../utils'
import { getStoryblokUrl } from '../../utils/api-routes'
import type { StoryblokLoginResponse, StoryblokLoginWithOtpResponse, StoryblokUser } from '../../types'

export const loginWithToken = async (token: string, region: RegionCode) => {
try {
return await ofetch(`https://${regionsDomain[region]}/v1/users/me`, {
const url = getStoryblokUrl(region)
return await customFetch<{
user: StoryblokUser
}>(`${url}/users/me`, {
headers: {
Authorization: token,
},
})
}
catch (error) {
if (error instanceof FetchError) {
const status = error.response?.status
const status = error.response.status

switch (status) {
case 401:
Expand All @@ -24,17 +28,16 @@ export const loginWithToken = async (token: string, region: RegionCode) => {
throw new APIError('network_error', 'login_with_token', error)
}
}
else {
throw new APIError('generic', 'login_with_token', error as Error)
}
throw new APIError('generic', 'login_with_token', error as FetchError)
}
}

export const loginWithEmailAndPassword = async (email: string, password: string, region: RegionCode) => {
try {
return await ofetch(`https://${regionsDomain[region]}/v1/users/login`, {
const url = getStoryblokUrl(region)
return await customFetch<StoryblokLoginResponse>(`${url}/users/login`, {
method: 'POST',
body: JSON.stringify({ email, password }),
body: { email, password },
})
}
catch (error) {
Expand All @@ -44,9 +47,10 @@ export const loginWithEmailAndPassword = async (email: string, password: string,

export const loginWithOtp = async (email: string, password: string, otp: string, region: RegionCode) => {
try {
return await ofetch(`https://${regionsDomain[region]}/v1/users/login`, {
const url = getStoryblokUrl(region)
return await customFetch<StoryblokLoginWithOtpResponse>(`${url}/users/login`, {
method: 'POST',
body: JSON.stringify({ email, password, otp_attempt: otp }),
body: { email, password, otp_attempt: otp },
})
}
catch (error) {
Expand Down
10 changes: 6 additions & 4 deletions src/commands/login/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,18 @@ export const loginCommand = program
})
const response = await loginWithEmailAndPassword(userEmail, userPassword, userRegion)

if (response.otp_required) {
if (response?.otp_required) {
const otp = await input({
message: 'Add the code from your Authenticator app, or the one we sent to your e-mail / phone:',
required: true,
})

const { access_token } = await loginWithOtp(userEmail, userPassword, otp, userRegion)
updateSession(userEmail, access_token, userRegion)
const otpResponse = await loginWithOtp(userEmail, userPassword, otp, userRegion)
if (otpResponse?.access_token) {
updateSession(userEmail, otpResponse?.access_token, userRegion)
}
}
else {
else if (response?.access_token) {
updateSession(userEmail, response.access_token, userRegion)
}
await persistCredentials(region)
Expand Down
29 changes: 13 additions & 16 deletions src/commands/pull-languages/actions.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,35 @@
import { join } from 'node:path'

import { handleAPIError, handleFileSystemError } from '../../utils'
import { ofetch } from 'ofetch'
import { regionsDomain } from '../../constants'
import type { FetchError } from '../../utils/fetch'
import { customFetch } from '../../utils/fetch'
import { resolvePath, saveToFile } from '../../utils/filesystem'
import type { PullLanguagesOptions } from './constants'
import type { RegionCode } from '../../constants'
import type { SpaceInternationalization } from '../../types'
import { getStoryblokUrl } from '../../utils/api-routes'

export interface SpaceInternationalizationOptions {
languages: SpaceLanguage[]
default_lang_name: string
}
export interface SpaceLanguage {
code: string
name: string
}

export const pullLanguages = async (space: string, token: string, region: string): Promise<SpaceInternationalizationOptions | undefined> => {
export const pullLanguages = async (space: string, token: string, region: RegionCode): Promise<SpaceInternationalization | undefined> => {
try {
const response = await ofetch(`https://${regionsDomain[region]}/v1/spaces/${space}`, {
const url = getStoryblokUrl(region)
const response = await customFetch<{
space: SpaceInternationalization
}>(`${url}/spaces/${space}`, {
headers: {
Authorization: token,
},
})

return {
default_lang_name: response.space.default_lang_name,
languages: response.space.languages,
}
}
catch (error) {
handleAPIError('pull_languages', error as Error)
handleAPIError('pull_languages', error as FetchError)
}
}

export const saveLanguagesToFile = async (space: string, internationalizationOptions: SpaceInternationalizationOptions, options: PullLanguagesOptions) => {
export const saveLanguagesToFile = async (space: string, internationalizationOptions: SpaceInternationalization, options: PullLanguagesOptions) => {
try {
const { filename = 'languages', suffix = space, path } = options
const data = JSON.stringify(internationalizationOptions, null, 2)
Expand Down
20 changes: 12 additions & 8 deletions src/commands/user/actions.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { FetchError, ofetch } from 'ofetch'
import { regionsDomain } from '../../constants'
import chalk from 'chalk'
import type { RegionCode } from '../../constants'
import { customFetch, FetchError } from '../../utils/fetch'
import { APIError, maskToken } from '../../utils'
import { getStoryblokUrl } from '../../utils/api-routes'
import type { StoryblokUser } from '../../types'

export const getUser = async (token: string, region: string) => {
export const getUser = async (token: string, region: RegionCode) => {
try {
return await ofetch(`https://${regionsDomain[region]}/v1/users/me`, {
const url = getStoryblokUrl(region)
const response = await customFetch<{
user: StoryblokUser
}>(`${url}/users/me`, {
headers: {
Authorization: token,
},
})
return response
}
catch (error) {
if (error instanceof FetchError) {
const status = error.response?.status
const status = error.response.status

switch (status) {
case 401:
Expand All @@ -23,8 +29,6 @@ export const getUser = async (token: string, region: string) => {
throw new APIError('network_error', 'get_user', error)
}
}
else {
throw new APIError('generic', 'get_user', error as Error)
}
throw new APIError('generic', 'get_user', error as FetchError)
}
}
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ program.command('test').action(async () => {
const verbose = program.opts().verbose
try {
const { state, initializeSession } = session()

await initializeSession()

console.log(state)
if (!state.password) {
throw new Error('No password found')
}
}
catch (error) {
handleError(error as Error, verbose)
Expand Down
6 changes: 3 additions & 3 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface SessionState {
isLoggedIn: boolean
login?: string
password?: string
region?: string
region?: RegionCode
envLogin?: boolean
}

Expand All @@ -24,7 +24,7 @@ function createSession() {
state.isLoggedIn = true
state.login = envCredentials.login
state.password = envCredentials.password
state.region = envCredentials.region
state.region = envCredentials.region as RegionCode
state.envLogin = true
return
}
Expand All @@ -36,7 +36,7 @@ function createSession() {
state.isLoggedIn = true
state.login = creds.login
state.password = creds.password
state.region = creds.region
state.region = creds.region as RegionCode
}
else {
// No credentials found; set state to logged out
Expand Down
10 changes: 10 additions & 0 deletions src/types/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Interface representing an HTTP response error
*/
export interface ResponseError extends Error {
response?: {
status: number
statusText: string
data?: any
}
}
42 changes: 42 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,45 @@ export interface CommandOptions {
*/
verbose: boolean
}

// All these types should come from a general package

/**
* Interface representing a language in Storyblok
*/
export interface Language {
name: string
code: string
fallback_code?: string
ai_translation_code: string | null
}

export interface SpaceInternationalization {
languages: Language[]
default_lang_name: string
}

export interface StoryblokUser {
id: number
email: string
username: string
friendly_name: string
otp_required: boolean
access_token: string
}

export interface StoryblokLoginResponse {
otp_required: boolean
login_strategy: string
configured_2fa_options: string[]
access_token?: string
}

export interface StoryblokLoginWithOtpResponse {
access_token: string
email: string
token_type: string
user_id: number
role: string
has_partner: boolean
}
Loading

0 comments on commit 27f97da

Please sign in to comment.