@basictech/cli
Version:
Basic CLI for creating & managing your projects
288 lines • 9.86 kB
JavaScript
import { createServer } from 'http';
import { promises as fs } from 'fs';
import { CONSTANTS } from './constants.js';
import { getConfigPath, getConfigDir, openBrowser } from './platform.js';
import { AuthError, handleError } from './errors.js';
export class AuthService {
static instance;
static getInstance() {
if (!AuthService.instance) {
AuthService.instance = new AuthService();
}
return AuthService.instance;
}
async login() {
try {
const token = await this.startOAuthFlow();
await this.saveToken(token);
}
catch (error) {
throw handleError(error);
}
}
async logout() {
try {
await this.deleteToken();
}
catch (error) {
throw handleError(error);
}
}
async getToken() {
try {
const tokenPath = getConfigPath();
const tokenData = await fs.readFile(tokenPath, 'utf8');
const token = JSON.parse(tokenData);
// Check if token is expired
if (token.expires_at && Date.now() > token.expires_at) {
const refreshedToken = await this.refreshToken(token);
await this.saveToken(refreshedToken);
return refreshedToken;
}
return token;
}
catch (error) {
if (error.code === 'ENOENT') {
return null; // Token file doesn't exist
}
throw handleError(error);
}
}
async getUserInfo() {
const token = await this.getToken();
if (!token) {
throw new AuthError('Not logged in');
}
const response = await fetch(`${CONSTANTS.API_BASE}/auth/userInfo`, {
headers: {
Authorization: `Bearer ${token.access_token}`,
},
});
if (!response.ok) {
throw new AuthError('Failed to fetch user info');
}
return await response.json();
}
async startOAuthFlow() {
return new Promise((resolve, reject) => {
const server = createServer((req, res) => {
if (req.url?.startsWith('/callback')) {
const url = new URL(req.url, CONSTANTS.OAUTH_REDIRECT);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
if (!code) {
res.writeHead(400);
res.end('Authorization code not found');
server.close();
reject(new AuthError('Authorization code not found'));
return;
}
this.exchangeCodeForToken(code)
.then(token => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(this.getSuccessHtml());
server.close();
resolve(token);
})
.catch(error => {
res.writeHead(500);
res.end('Authentication failed');
server.close();
reject(error);
});
}
else {
res.writeHead(404);
res.end('Not found');
}
});
server.listen(8080, async () => {
const state = this.generateState();
const authUrl = `${CONSTANTS.API_BASE}/auth/authorize?` +
`client_id=${CONSTANTS.OAUTH_CLIENT_ID}&` +
`redirect_uri=${encodeURIComponent(CONSTANTS.OAUTH_REDIRECT)}&` +
`response_type=code&` +
`scope=${encodeURIComponent(CONSTANTS.OAUTH_SCOPES)}&` +
`state=${state}`;
try {
await openBrowser(authUrl);
}
catch (error) {
console.log(`Please visit this URL to log in: ${authUrl}`);
}
});
// Timeout after 5 minutes
setTimeout(() => {
server.close();
reject(new AuthError('Authentication timeout'));
}, 5 * 60 * 1000);
});
}
async exchangeCodeForToken(code) {
const response = await fetch(`${CONSTANTS.API_BASE}/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: CONSTANTS.OAUTH_CLIENT_ID,
code,
redirect_uri: CONSTANTS.OAUTH_REDIRECT,
}),
});
if (!response.ok) {
throw new AuthError('Failed to exchange code for token');
}
const data = await response.json();
return {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_at: Date.now() + (data.expires_in * 1000),
token_type: data.token_type || 'Bearer',
};
}
async refreshToken(token) {
const response = await fetch(`${CONSTANTS.API_BASE}/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: CONSTANTS.OAUTH_CLIENT_ID,
code: token.refresh_token,
}),
});
if (!response.ok) {
throw new AuthError('Failed to refresh token', [
'Try logging in again with \'basic login\'',
]);
}
const data = await response.json();
return {
access_token: data.access_token,
refresh_token: data.refresh_token || token.refresh_token,
expires_at: Date.now() + (data.expires_in * 1000),
token_type: data.token_type || 'Bearer',
};
}
async saveToken(token) {
const tokenPath = getConfigPath();
const configDir = getConfigDir();
// Create directory if it doesn't exist
await fs.mkdir(configDir, { recursive: true });
// Write token with restrictive permissions
await fs.writeFile(tokenPath, JSON.stringify(token, null, 2), { mode: 0o600 });
}
async deleteToken() {
const tokenPath = getConfigPath();
try {
await fs.unlink(tokenPath);
}
catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
}
generateState() {
const array = new Uint8Array(24);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
getSuccessHtml() {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Basic CLI Authentication</title>
<style>
:root { color-scheme: light dark; }
@media (prefers-color-scheme: light) {
:root {
--bg-color: #f5f5f5;
--container-bg: #ffffff;
--text-color: #000000;
--shadow: 0 2px 4px rgba(0,0,0,0.1);
}
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a;
--container-bg: #2d2d2d;
--text-color: #ffffff;
--shadow: 0 2px 4px rgba(0,0,0,0.3);
}
}
body {
font-family: monospace;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: var(--bg-color);
color: var(--text-color);
}
.container {
text-align: center;
padding: 2rem;
background: var(--container-bg);
border-radius: 8px;
box-shadow: var(--shadow);
}
.success-icon {
color: #AE87FF;
font-size: 32px;
margin-bottom: 1rem;
}
h2 { margin: 0 0 1rem 0; }
p { margin: 0; opacity: 0.8; }
.help-text {
margin-top: 1.5rem;
font-size: 0.9em;
opacity: 0.7;
text-align: left;
}
.help-text ol {
margin: 0;
padding-left: 1.5rem;
}
.help-text li { margin: 0.3rem 0; }
code {
background: var(--bg-color);
padding: 0.2em 0.4em;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
a {
color: #AE87FF;
text-decoration: none;
}
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<div class="success-icon">✅</div>
<h2>Authentication Successful!</h2>
<p>You can close this window and return to the CLI.</p>
<div class="help-text">
<ol>
<li>Use command <code>basic help</code> to get started with the CLI</li>
<li>Visit the <a href="https://docs.basic.tech" target="_blank">Basic docs</a> for more info</li>
</ol>
</div>
</div>
<script>
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
`;
}
}
//# sourceMappingURL=auth.js.map