478 lines
12 KiB
TypeScript
478 lines
12 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
|
|
export interface IIdpCliConfig {
|
|
idpBaseUrl: string;
|
|
configDir?: string;
|
|
}
|
|
|
|
export interface IStoredCredentials {
|
|
refreshToken?: string;
|
|
jwt?: string;
|
|
userId?: string;
|
|
}
|
|
|
|
/**
|
|
* IdpCli - A Node.js CLI client for idp.global
|
|
* Uses file-based storage instead of browser webstore
|
|
*/
|
|
export class IdpCli {
|
|
public config: IIdpCliConfig;
|
|
public configDir: string;
|
|
public credentialsPath: string;
|
|
|
|
public typedsocket: plugins.typedsocket.TypedSocket;
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
private typedsocketDeferred = plugins.smartpromise.defer<plugins.typedsocket.TypedSocket>();
|
|
|
|
constructor(configArg: IIdpCliConfig) {
|
|
this.config = configArg;
|
|
this.configDir = configArg.configDir || plugins.path.join(plugins.os.homedir(), '.idp-global');
|
|
this.credentialsPath = plugins.path.join(this.configDir, 'credentials.json');
|
|
}
|
|
|
|
/**
|
|
* Ensure config directory exists
|
|
*/
|
|
private ensureConfigDir(): void {
|
|
if (!plugins.fs.existsSync(this.configDir)) {
|
|
plugins.fs.mkdirSync(this.configDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store credentials to file
|
|
*/
|
|
public storeCredentials(credentials: IStoredCredentials): void {
|
|
this.ensureConfigDir();
|
|
plugins.fs.writeFileSync(this.credentialsPath, JSON.stringify(credentials, null, 2), 'utf8');
|
|
}
|
|
|
|
/**
|
|
* Load stored credentials
|
|
*/
|
|
public loadCredentials(): IStoredCredentials | null {
|
|
try {
|
|
if (!plugins.fs.existsSync(this.credentialsPath)) {
|
|
return null;
|
|
}
|
|
const content = plugins.fs.readFileSync(this.credentialsPath, 'utf8');
|
|
return JSON.parse(content);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete stored credentials (logout)
|
|
*/
|
|
public deleteCredentials(): void {
|
|
try {
|
|
if (plugins.fs.existsSync(this.credentialsPath)) {
|
|
plugins.fs.unlinkSync(this.credentialsPath);
|
|
}
|
|
} catch {
|
|
// ignore if file doesn't exist
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Connect to IDP server via WebSocket
|
|
*/
|
|
public async connect(): Promise<void> {
|
|
if (this.typedsocketDeferred.status === 'fulfilled') {
|
|
return;
|
|
}
|
|
|
|
let baseUrl = this.config.idpBaseUrl;
|
|
if (baseUrl.endsWith('/')) {
|
|
baseUrl = baseUrl.slice(0, -1);
|
|
}
|
|
if (!baseUrl.endsWith('/typedrequest')) {
|
|
baseUrl = `${baseUrl}/typedrequest`;
|
|
}
|
|
|
|
console.log(`Connecting to ${baseUrl}...`);
|
|
this.typedsocket = await plugins.typedsocket.TypedSocket.createClient(
|
|
this.typedrouter,
|
|
baseUrl
|
|
);
|
|
this.typedsocketDeferred.resolve(this.typedsocket);
|
|
console.log('Connected!');
|
|
}
|
|
|
|
/**
|
|
* Disconnect from IDP server
|
|
*/
|
|
public async disconnect(): Promise<void> {
|
|
if (this.typedsocket) {
|
|
await this.typedsocket.stop();
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// Authentication Commands
|
|
// ============================================
|
|
|
|
/**
|
|
* Login with email and password
|
|
*/
|
|
public async loginWithPassword(email: string, password: string): Promise<boolean> {
|
|
await this.connect();
|
|
|
|
const loginRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmailOrUsernameAndPassword>(
|
|
'loginWithEmailOrUsernameAndPassword'
|
|
);
|
|
|
|
const response = await loginRequest.fire({
|
|
username: email,
|
|
password: password,
|
|
});
|
|
|
|
if (response.refreshToken) {
|
|
this.storeCredentials({
|
|
refreshToken: response.refreshToken,
|
|
});
|
|
console.log('Login successful!');
|
|
return true;
|
|
} else if (response.twoFaNeeded) {
|
|
console.log('Two-factor authentication required.');
|
|
return false;
|
|
} else {
|
|
console.log('Login failed.');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Login with API token
|
|
*/
|
|
public async loginWithApiToken(apiToken: string): Promise<boolean> {
|
|
await this.connect();
|
|
|
|
const loginRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_LoginWithApiToken>(
|
|
'loginWithApiToken'
|
|
);
|
|
|
|
const response = await loginRequest.fire({
|
|
apiToken,
|
|
});
|
|
|
|
if (response.jwt) {
|
|
this.storeCredentials({
|
|
jwt: response.jwt,
|
|
});
|
|
console.log('Login successful!');
|
|
return true;
|
|
} else {
|
|
console.log('Login failed.');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh JWT from stored refresh token
|
|
*/
|
|
public async refreshJwt(): Promise<string | null> {
|
|
const credentials = this.loadCredentials();
|
|
if (!credentials?.refreshToken) {
|
|
console.error('No refresh token stored. Please login first.');
|
|
return null;
|
|
}
|
|
|
|
await this.connect();
|
|
|
|
const refreshRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
|
|
'refreshJwt'
|
|
);
|
|
|
|
const response = await refreshRequest.fire({
|
|
refreshToken: credentials.refreshToken,
|
|
});
|
|
|
|
if (response.jwt) {
|
|
this.storeCredentials({
|
|
...credentials,
|
|
jwt: response.jwt,
|
|
});
|
|
return response.jwt;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Logout - clear stored credentials
|
|
*/
|
|
public async logout(): Promise<void> {
|
|
const credentials = this.loadCredentials();
|
|
|
|
if (credentials?.refreshToken) {
|
|
try {
|
|
await this.connect();
|
|
const logoutRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.ILogoutRequest>(
|
|
'logout'
|
|
);
|
|
await logoutRequest.fire({
|
|
refreshToken: credentials.refreshToken,
|
|
});
|
|
} catch (e) {
|
|
// Ignore errors during server-side logout
|
|
}
|
|
}
|
|
|
|
this.deleteCredentials();
|
|
console.log('Logged out successfully.');
|
|
}
|
|
|
|
// ============================================
|
|
// User Commands
|
|
// ============================================
|
|
|
|
/**
|
|
* Get current user info
|
|
*/
|
|
public async whoami(): Promise<plugins.idpInterfaces.data.IUser | null> {
|
|
const jwt = await this.ensureAuthenticated();
|
|
if (!jwt) return null;
|
|
|
|
await this.connect();
|
|
|
|
const whoIsRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_WhoIs>(
|
|
'whoIs'
|
|
);
|
|
|
|
const response = await whoIsRequest.fire({ jwt });
|
|
return response.user;
|
|
}
|
|
|
|
/**
|
|
* Get user sessions
|
|
*/
|
|
public async getSessions(): Promise<plugins.idpInterfaces.request.IReq_GetUserSessions['response']['sessions'] | null> {
|
|
const jwt = await this.ensureAuthenticated();
|
|
if (!jwt) return null;
|
|
|
|
await this.connect();
|
|
|
|
const sessionsRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetUserSessions>(
|
|
'getUserSessions'
|
|
);
|
|
|
|
const response = await sessionsRequest.fire({ jwt });
|
|
return response.sessions;
|
|
}
|
|
|
|
/**
|
|
* Revoke a session
|
|
*/
|
|
public async revokeSession(sessionId: string): Promise<boolean> {
|
|
const jwt = await this.ensureAuthenticated();
|
|
if (!jwt) return false;
|
|
|
|
await this.connect();
|
|
|
|
const revokeRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RevokeSession>(
|
|
'revokeSession'
|
|
);
|
|
|
|
const response = await revokeRequest.fire({ jwt, sessionId });
|
|
return response.success;
|
|
}
|
|
|
|
// ============================================
|
|
// Organization Commands
|
|
// ============================================
|
|
|
|
/**
|
|
* Get organizations for current user
|
|
*/
|
|
public async getOrganizations(): Promise<{
|
|
roles: plugins.idpInterfaces.data.IRole[];
|
|
organizations: plugins.idpInterfaces.data.IOrganization[];
|
|
} | null> {
|
|
const jwt = await this.ensureAuthenticated();
|
|
if (!jwt) return null;
|
|
|
|
const user = await this.whoami();
|
|
if (!user) return null;
|
|
|
|
await this.connect();
|
|
|
|
const orgsRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetRolesAndOrganizationsForUserId>(
|
|
'getRolesAndOrganizationsForUserId'
|
|
);
|
|
|
|
const response = await orgsRequest.fire({
|
|
jwt,
|
|
userId: user.id,
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Create a new organization
|
|
*/
|
|
public async createOrganization(
|
|
name: string,
|
|
slug: string,
|
|
mode: 'checkAvailability' | 'manifest' = 'manifest'
|
|
): Promise<plugins.idpInterfaces.request.IReq_CreateOrganization['response'] | null> {
|
|
const jwt = await this.ensureAuthenticated();
|
|
if (!jwt) return null;
|
|
|
|
const user = await this.whoami();
|
|
if (!user) return null;
|
|
|
|
await this.connect();
|
|
|
|
const createRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CreateOrganization>(
|
|
'createOrganization'
|
|
);
|
|
|
|
const response = await createRequest.fire({
|
|
jwt,
|
|
userId: user.id,
|
|
organizationName: name,
|
|
organizationSlug: slug,
|
|
action: mode,
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Get organization members
|
|
*/
|
|
public async getOrgMembers(
|
|
organizationId: string
|
|
): Promise<plugins.idpInterfaces.request.IReq_GetOrgMembers['response']['members'] | null> {
|
|
const jwt = await this.ensureAuthenticated();
|
|
if (!jwt) return null;
|
|
|
|
await this.connect();
|
|
|
|
const membersRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetOrgMembers>(
|
|
'getOrgMembers'
|
|
);
|
|
|
|
const response = await membersRequest.fire({
|
|
jwt,
|
|
organizationId,
|
|
});
|
|
|
|
return response.members;
|
|
}
|
|
|
|
/**
|
|
* Invite a user to organization
|
|
*/
|
|
public async inviteMember(
|
|
organizationId: string,
|
|
email: string,
|
|
roles: string[] = ['member']
|
|
): Promise<plugins.idpInterfaces.request.IReq_CreateInvitation['response'] | null> {
|
|
const jwt = await this.ensureAuthenticated();
|
|
if (!jwt) return null;
|
|
|
|
await this.connect();
|
|
|
|
const inviteRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CreateInvitation>(
|
|
'createInvitation'
|
|
);
|
|
|
|
const response = await inviteRequest.fire({
|
|
jwt,
|
|
organizationId,
|
|
email,
|
|
roles,
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
// ============================================
|
|
// Admin Commands
|
|
// ============================================
|
|
|
|
/**
|
|
* Check if current user is global admin
|
|
*/
|
|
public async checkGlobalAdmin(): Promise<boolean> {
|
|
const jwt = await this.ensureAuthenticated();
|
|
if (!jwt) return false;
|
|
|
|
await this.connect();
|
|
|
|
const adminRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CheckGlobalAdmin>(
|
|
'checkGlobalAdmin'
|
|
);
|
|
|
|
const response = await adminRequest.fire({ jwt });
|
|
return response.isGlobalAdmin;
|
|
}
|
|
|
|
/**
|
|
* Get global app statistics (admin only)
|
|
*/
|
|
public async getGlobalAppStats(): Promise<plugins.idpInterfaces.request.IReq_GetGlobalAppStats['response']['apps'] | null> {
|
|
const jwt = await this.ensureAuthenticated();
|
|
if (!jwt) return null;
|
|
|
|
await this.connect();
|
|
|
|
const statsRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalAppStats>(
|
|
'getGlobalAppStats'
|
|
);
|
|
|
|
const response = await statsRequest.fire({ jwt });
|
|
return response.apps;
|
|
}
|
|
|
|
/**
|
|
* Suspend a user (admin only)
|
|
*/
|
|
public async suspendUser(userId: string): Promise<boolean> {
|
|
const jwt = await this.ensureAuthenticated();
|
|
if (!jwt) return false;
|
|
|
|
await this.connect();
|
|
|
|
const suspendRequest = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_SuspendUser>(
|
|
'suspendUser'
|
|
);
|
|
|
|
await suspendRequest.fire({ jwt, userId });
|
|
return true;
|
|
}
|
|
|
|
// ============================================
|
|
// Helpers
|
|
// ============================================
|
|
|
|
/**
|
|
* Ensure user is authenticated, refresh JWT if needed
|
|
*/
|
|
private async ensureAuthenticated(): Promise<string | null> {
|
|
let credentials = this.loadCredentials();
|
|
|
|
if (!credentials) {
|
|
console.error('Not logged in. Please run: idp login');
|
|
return null;
|
|
}
|
|
|
|
// If we have a JWT, return it
|
|
if (credentials.jwt) {
|
|
return credentials.jwt;
|
|
}
|
|
|
|
// Otherwise, try to get a new JWT from refresh token
|
|
if (credentials.refreshToken) {
|
|
const jwt = await this.refreshJwt();
|
|
return jwt;
|
|
}
|
|
|
|
console.error('No valid credentials. Please run: idp login');
|
|
return null;
|
|
}
|
|
}
|