@teamwork/get-bearer-token
Version:
CLI tool to obtain bearer tokens for Teamwork API using OAuth flow
171 lines (148 loc) • 4.92 kB
JavaScript
import express from "express";
import path from "path";
import { fileURLToPath } from "url";
import axios from "axios";
import {
exchangeCodeForToken,
setAuthData,
getAuthData,
isAuthenticated,
clearAuthData,
buildAuthUrl
} from "./auth.js";
export async function startServer({ port = 8123, version = '0.0.0' } = {}) {
const app = express();
// Resolve __dirname in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Enable JSON body parsing
app.use(express.json());
// Login redirect - opens Teamwork launchpad in the browser
app.get("/api/auth/login", (req, res) => {
const authUrl = buildAuthUrl();
res.redirect(authUrl);
});
// App-specific login redirect - for generating app tokens
app.get("/api/auth/app-login", (req, res) => {
const { clientId } = req.query;
if (!clientId) return res.status(400).json({ error: "clientId is required" });
const authUrl = buildAuthUrl(clientId);
res.redirect(authUrl);
});
// Generic proxy for Teamwork API calls - injects auth header
const proxyPrefix = "/api/proxy/teamwork/";
app.all(`${proxyPrefix}{*path}`, async (req, res) => {
if (!isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const authData = getAuthData();
const baseUrl = authData.installation.url;
const targetPath = req.originalUrl.slice(proxyPrefix.length);
const targetUrl = `${baseUrl}${targetPath}`;
try {
const response = await axios({
method: req.method,
url: targetUrl,
data: req.body,
headers: {
"Authorization": `Bearer ${authData.access_token}`,
"Content-Type": "application/json",
"Accept": "application/json",
},
});
res.status(response.status).json(response.data);
} catch (error) {
const status = error.response?.status || 500;
const data = error.response?.data || { error: error.message };
res.status(status).json(data);
}
});
// Serve static files
app.use("/", express.static(path.join(__dirname, "public")));
// API endpoint to exchange code for token
app.post("/api/auth/token", async (req, res) => {
const { code } = req.body;
if (!code) {
return res.status(400).json({ error: "Code is required" });
}
try {
// Exchange code for access token via Teamwork Launchpad
const data = await exchangeCodeForToken(code);
// Store auth data in memory
setAuthData(data);
res.json({ success: true, data, version });
} catch (error) {
console.error("Token exchange error:", error.message);
res.status(500).json({
error: "Failed to exchange code for token",
details: error.message,
});
}
});
// API endpoint to get current auth status
app.get("/api/auth/status", (req, res) => {
if (isAuthenticated()) {
const authData = getAuthData();
res.json({
authenticated: true,
installation: authData.installation,
user: authData.user,
accessToken: authData.access_token,
version,
});
} else {
res.json({ authenticated: false, version });
}
});
// API endpoint to logout
app.post("/api/auth/logout", (req, res) => {
clearAuthData();
res.json({ success: true });
});
// API endpoint to exchange code for app-specific token
app.post("/api/auth/app-token", async (req, res) => {
const { code, clientId, clientSecret, redirectUri } = req.body;
if (!code || !clientId || !clientSecret) {
return res.status(400).json({ error: "code, clientId, and clientSecret are required" });
}
try {
const authData = getAuthData();
if (!authData || !authData.installation) {
return res.status(401).json({ error: "Not authenticated" });
}
const baseUrl = authData.installation.url;
// Exchange code for access token using the app's credentials
const response = await axios.post(
`${baseUrl}launchpad/v1/token.json`,
{
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
},
{
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
}
);
res.json({ success: true, data: response.data });
} catch (error) {
console.error("App token exchange error:", error.response?.data || error.message);
res.status(500).json({
error: "Failed to exchange code for app token",
details: error.response?.data || error.message,
});
}
});
return new Promise((resolve, reject) => {
const server = app.listen(port, (err) => {
if (!err) {
resolve(server);
} else {
reject(err);
}
});
});
}