|
import express from 'express' |
|
import bodyParser from 'body-parser' |
|
import * as jwt from 'atlassian-jwt' |
|
import fetch from 'node-fetch' |
|
import fs from 'fs/promises' |
|
|
|
import descriptor from './atlassian-connect.json' |
|
|
|
const { |
|
CLIENT_SECRET = '', |
|
PUBLIC_URL = '', |
|
} = process.env |
|
|
|
const app = express() |
|
|
|
app.use(bodyParser.json()) |
|
|
|
app.get('/', (req, res) => { |
|
console.log('/') |
|
res.redirect('/atlassian-connect.json') |
|
}) |
|
|
|
app.post('/uninstalled', (req, res) => { |
|
console.log('uninstalled') |
|
console.log(req.body) |
|
res.sendStatus(200) |
|
}) |
|
|
|
app.post('/installed', (req, res) => { |
|
console.log('installed') |
|
console.log(req.body) // store at least { clientKey } |
|
res.sendStatus(200) |
|
}) |
|
|
|
app.get('/atlassian-connect.json', (req, res) => { |
|
console.log('descriptor') |
|
descriptor.baseUrl = PUBLIC_URL |
|
res.json(descriptor) |
|
}) |
|
|
|
app.listen(3000, () => { |
|
console.log('listening on port 3000') |
|
}) |
|
|
|
const qs = (params: Record<string, any>) => { |
|
const sp = new URLSearchParams(params) |
|
return sp.toString() |
|
} |
|
|
|
const getAccessToken = async (key: string, clientKey: string, secret: string) => { |
|
const baseUrl = 'https://bitbucket.org' |
|
const url = 'https://bitbucket.org/site/oauth2/access_token' |
|
|
|
const opts = { |
|
method: 'post' as 'post', |
|
headers: { |
|
'Content-Type': 'application/x-www-form-urlencoded', |
|
}, |
|
body: { |
|
grant_type: 'urn:bitbucket:oauth2:jwt', |
|
}, |
|
} |
|
|
|
const req = jwt.fromMethodAndPathAndBody(opts.method, url, opts.body) |
|
const qsh = jwt.createQueryStringHash(req, true, baseUrl) |
|
|
|
const now = Math.floor(Date.now() / 1000) |
|
const exp = now + (3 * 60) // expires in 3 minutes |
|
const tokenData = { |
|
iss: key, |
|
iat: now, // the time the token is generated |
|
exp, // token expiry time |
|
sub: clientKey, // clientKey from /installed |
|
qsh, |
|
} |
|
|
|
const jwtToken = jwt.encodeSymmetric(tokenData, secret) |
|
const options = { |
|
method: opts.method, |
|
headers: { |
|
...opts.headers, |
|
Authorization: `JWT ${jwtToken}` |
|
}, |
|
body: qs(opts.body), |
|
} |
|
const res = await fetch(url, options) |
|
if (res.status !== 200) { |
|
throw new Error(`Failed to get access token: ${res.status}`) |
|
} |
|
return res.json() |
|
} |
|
|
|
const downloadRepo = async (owner: string, name: string, branch: string, accessToken: string) => { |
|
const url = `https://bitbucket.org/${owner}/${name}/get/${branch}.zip` |
|
const res = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${accessToken}`, |
|
}, |
|
}) |
|
|
|
if (res.status !== 200) { |
|
throw new Error(`Failed to download repo: ${res.status}`) |
|
} |
|
|
|
const zip = await res.buffer() |
|
return zip |
|
} |
|
|
|
const go = async (clientKey: string, repoOwner: string, repoName: string, branchName: string) => { |
|
const token = await getAccessToken(descriptor.key, clientKey, CLIENT_SECRET) |
|
const zipBuffer = await downloadRepo(repoOwner, repoName, branchName, token.access_token) |
|
await fs.writeFile(`${repoOwner}-${repoName}-${branchName}.zip`, zipBuffer) |
|
} |
|
|
|
// go().then(console.log).catch(console.error) |