Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor login flow and hooks #28

Closed
wants to merge 12 commits into from
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
node_modules
dist
dist

.idea
.vscode
266 changes: 39 additions & 227 deletions source/commands/login.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import React, { useEffect, useState } from 'react';
import { Text } from 'ink';
import SelectInput from 'ink-select-input';
import Spinner from 'ink-spinner';
import { type infer as zInfer, object, string } from 'zod';
import { option } from 'pastel';
import { apiCall } from '../lib/api.js';
import {
authCallbackServer,
browserAuth,
saveAuthToken,
TokenType,
tokenType,
} from '../lib/auth.js';
import LoginFlow from '../components/LoginFlow.js';
import EnvironmentSelection, { ActiveState } from '../components/EnvironmentSelection.js';

export const options = object({
key: string()
Expand All @@ -31,245 +26,62 @@ export const options = object({
),
});

type Org = {
label: string;
value: string;
};

type Project = {
label: string;
value: string;
};

type Environment = {
label: string;
value: string;
};

type Props = {
readonly options: zInfer<typeof options>;
};

export default function Login({ options: { key, workspace } }: Props) {
const [authError, setAuthError] = useState<string>('');
const [orgs, setOrgs] = useState<Org[]>([]);
const [accessToken, setAccessToken] = useState<string | undefined>();
const [cookie, setCookie] = useState<string | undefined>('');
const [activeOrg, setActiveOrg] = useState<Org | null>(null);
const [activeProject, setActiveProject] = useState<Project | null>(null);
const [activeEnvironment, setActiveEnvironment] =
useState<Environment | null>(null);
const [projects, setProjects] = useState<Project[]>([]);
const [environments, setEnvironments] = useState<Environment[]>([]);
const [state, setState] = useState<
'login' | 'loggingIn' | 'org' | 'project' | 'environment' | 'done'
>('login');

useEffect(() => {
const fetchOrgs = async () => {
const { response: orgs } = await apiCall(
'v2/orgs',
accessToken ?? '',
cookie,
);

const selectedOrg = Array.isArray(orgs)
? orgs.find(
(org: { key: string }) => workspace && org.key === workspace,
)
: null;

if (selectedOrg) {
setActiveOrg({ label: selectedOrg.name, value: selectedOrg.id });
setState('project');
} else if (Array.isArray(orgs) && orgs.length === 1) {
setActiveOrg({ label: orgs[0].name, value: orgs[0].id });
setState('project');
}
const [state, setState] = useState<'login' | 'env' | 'done'>('login');
const [accessToken, setAccessToken] = useState<string>('');
const [cookie, setCookie] = useState<string>('');
const [error, setError] = useState<string | null>(null);

setOrgs(
Array.isArray(orgs)
? orgs.map((org: { name: string; id: string }) => ({
label: org.name,
value: org.id,
}))
: [],
);
};

if (state === 'org' && accessToken) {
fetchOrgs();
}
}, [state, accessToken, cookie, workspace]);

useEffect(() => {
const fetchProjects = async () => {
let newCookie = cookie ?? '';

const { headers } = await apiCall(
`v2/auth/switch_org/${activeOrg?.value}`,
accessToken ?? '',
cookie ?? '',
'POST',
);

newCookie = headers.getSetCookie()[0] ?? '';
setCookie(newCookie);

const { response: projects } = await apiCall(
'v2/projects',
accessToken ?? '',
newCookie,
);

if (Array.isArray(projects) && projects.length === 1) {
setActiveProject({ label: projects[0].name, value: projects[0].id });
setState('environment');
}
const [organization, setOrganization] = useState<string>('');
const [environment, setEnvironment] = useState<string>('');

setProjects(
Array.isArray(projects)
? projects.map((project: { name: string; id: string }) => ({
label: project.name,
value: project.id,
}))
: [],
);
};

if (activeOrg) {
fetchProjects();
}
}, [activeOrg, accessToken, cookie]);

useEffect(() => {
const fetchEnvironments = async () => {
const { response: environments } = await apiCall(
`v2/projects/${activeProject?.value}/envs`,
accessToken ?? '',
cookie ?? '',
);
setEnvironments(
Array.isArray(environments)
? environments.map((environment: { name: string; id: string }) => ({
label: environment.name,
value: environment.id,
}))
: [],
);
};

if (activeProject) {
fetchEnvironments();
}
}, [activeProject, accessToken, cookie]);
const onEnvironmentSelectSuccess = async (organisation: ActiveState, _project: ActiveState, environment: ActiveState, secret: string) => {
setOrganization(organisation.label);
setEnvironment(environment.label);
await saveAuthToken(secret);
setState('done');
process.exit(1);
};

useEffect(() => {
if (state === 'done') {
process.exit(0);
if (error) {
process.exit(1);
}
}, [state]);
}, [error])

const handleOrgSelect = async (org: Org) => {
setActiveOrg(org);
setState('project');
const onLoginSuccess = (accessToken: string, cookie: string) => {
setAccessToken(accessToken);
setCookie(cookie);
setState('env');
};

useEffect(() => {
const authenticateUser = async () => {
setState('loggingIn');
if (key && tokenType(key) === TokenType.APIToken) {
setAccessToken(key);
} else if (key) {
setAuthError('Invalid API Key');
setState('done');
} else {
// Open the authentication URL in the default browser
const verifier = await browserAuth();
const token = await authCallbackServer(verifier);
const { headers } = await apiCall(
'v2/auth/login',
token ?? '',
'',
'POST',
);
setAccessToken(token);
setCookie(headers.getSetCookie()[0]);
}

setState('org');
};

authenticateUser();
}, [key]);
const onError = (error: string) => {
setError(error);
};

return (
<>
{state === 'login' && <Text>Login to Permit</Text>}
{state === 'loggingIn' && (
<Text>
<Spinner type="dots" /> Logging in...
</Text>
)}
{state === 'org' &&
(orgs && orgs.length > 0 ? (
<>
<Text>Select an organization</Text>
<SelectInput items={orgs} onSelect={handleOrgSelect} />
</>
) : (
<Text>
<Spinner type="dots" /> Loading Organizations
</Text>
))}
{state === 'project' &&
(projects && projects.length > 0 ? (
<>
<Text>Select a project</Text>
<SelectInput
items={projects}
onSelect={project => {
setActiveProject(project);
setState('environment');
}}
/>
</>
) : (
<Text>
<Spinner type="dots" /> Loading Projects
</Text>
))}
{state === 'environment' &&
(environments && environments.length > 0 ? (
<>
<Text>Select an environment</Text>
<SelectInput
items={environments}
onSelect={async environment => {
setActiveEnvironment(environment);
const { response } = await apiCall(
`v2/api-key/${activeProject?.value}/${environment.value}`,
accessToken ?? '',
cookie,
);
const { secret } = response as { secret: string };
await saveAuthToken(secret);
setState('done');
}}
/>
</>
) : (
<Text>
<Spinner type="dots" /> Loading Environments
</Text>
))}
{state === 'done' && activeOrg && (
{
state == 'login' && <LoginFlow apiKey={key} onSuccess={onLoginSuccess} onError={onError} />
}
{
state === 'env' &&
<EnvironmentSelection accessToken={accessToken} cookie={cookie} onComplete={onEnvironmentSelectSuccess}
onError={onError} workspace={workspace} />
}
{state === 'done' &&
<Text>
Logged in as {activeOrg.label} with selected environment as{' '}
{activeEnvironment ? activeEnvironment.label : 'None'}
Logged in as {organization} with selected environment as {environment}
</Text>
)}
{state === 'done' && authError && <Text>{authError}</Text>}
}
{
error && <Text>{error}</Text>
}
</>
);
}
Loading
Loading