mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 21:19:13 +00:00
Add endpoints to check account, and fetch projects
This commit is contained in:
parent
dd84f525fc
commit
8919703edc
@ -1,5 +1,6 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { BridgeConfigGitHub } from "../Config/Config";
|
||||
import { ApiError, ErrCode } from "../provisioning/api";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { generateGitHubOAuthUrl } from "./AdminCommands";
|
||||
|
||||
@ -13,6 +14,9 @@ export class GitHubProvisionerRouter {
|
||||
}
|
||||
|
||||
private onOAuth(req: Request<undefined, undefined, undefined, {userId: string}>, res: Response<{url: string}>) {
|
||||
if (!this.config.oauth) {
|
||||
throw new ApiError("GitHub is not configured to support OAuth", ErrCode.UnsupportedOperation);
|
||||
}
|
||||
res.send({
|
||||
url: generateGitHubOAuthUrl(this.config.oauth.client_id, this.config.oauth.redirect_uri, this.tokenStore.createStateForOAuth(req.query.userId))
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
import axios from 'axios';
|
||||
import axios, { Method } from 'axios';
|
||||
import JiraApi, { SearchUserOptions } from 'jira-client';
|
||||
import QuickLRU from "@alloc/quick-lru";
|
||||
import { JiraAccount, JiraAPIAccessibleResource, JiraIssue, JiraOAuthResult, JiraProject } from './Types';
|
||||
@ -10,23 +10,53 @@ const log = new LogWrapper("JiraClient");
|
||||
const ACCESSIBLE_RESOURCE_CACHE_LIMIT = 100;
|
||||
const ACCESSIBLE_RESOURCE_CACHE_TTL_MS = 60000;
|
||||
|
||||
interface JiraProjectSearchResponse {
|
||||
nextPage: string;
|
||||
maxResults: number;
|
||||
startAt: number;
|
||||
isLast: boolean;
|
||||
values: JiraProject[];
|
||||
}
|
||||
|
||||
export class HookshotJiraApi extends JiraApi {
|
||||
constructor(private options: JiraApi.JiraApiOptions) {
|
||||
constructor(private options: JiraApi.JiraApiOptions, private readonly res: JiraAPIAccessibleResource) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
public get resource() {
|
||||
return this.res;
|
||||
}
|
||||
|
||||
private async apiRequest<T>(path: string, method?: Method, data?: undefined): Promise<T>
|
||||
private async apiRequest<T, R>(path: string, method: Method, data?: R): Promise<T> {
|
||||
const url = `https://api.atlassian.com/${this.options.base}${path}`;
|
||||
const res = await axios.request<T>({ url,
|
||||
method: method || "GET",
|
||||
data,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.options.bearer}`
|
||||
},
|
||||
responseType: 'json',
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async getProject(projectIdOrKey: string): Promise<JiraProject> {
|
||||
return await super.getProject(projectIdOrKey) as JiraProject;
|
||||
}
|
||||
|
||||
async getIssue(issueIdOrKey: string): Promise<JiraIssue> {
|
||||
const res = await axios.get<JiraIssue>(`https://api.atlassian.com/${this.options.base}/rest/api/3/issue/${issueIdOrKey}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.options.bearer}`
|
||||
},
|
||||
responseType: 'json',
|
||||
});
|
||||
return res.data;
|
||||
return this.apiRequest<JiraIssue>(`/rest/api/3/issue/${issueIdOrKey}`);
|
||||
}
|
||||
|
||||
async * getAllProjects(status = "live"): AsyncIterable<JiraProject> {
|
||||
let response;
|
||||
let startAt = 0;
|
||||
do {
|
||||
response = await this.apiRequest<JiraProjectSearchResponse>(`/rest/api/3/project/search?startAt=${startAt}&status=${status}`);
|
||||
yield* response.values;
|
||||
startAt += response.maxResults;
|
||||
} while(!response.isLast)
|
||||
}
|
||||
|
||||
async searchUsers(opts: SearchUserOptions): Promise<JiraAccount[]> {
|
||||
@ -102,6 +132,14 @@ export class JiraClient {
|
||||
return this.getClientForResource(resource);
|
||||
}
|
||||
|
||||
async getClientForName(name: string) {
|
||||
const resource = (await this.getAccessibleResources()).find((r) => r.name === name);
|
||||
if (!resource) {
|
||||
return null;
|
||||
}
|
||||
return this.getClientForResource(resource);
|
||||
}
|
||||
|
||||
async getClientForResource(res: JiraAPIAccessibleResource) {
|
||||
// Check token age
|
||||
await this.checkTokenAge();
|
||||
@ -112,6 +150,6 @@ export class JiraClient {
|
||||
apiVersion: '3',
|
||||
strictSSL: true,
|
||||
bearer: this.bearer,
|
||||
});
|
||||
}, res);
|
||||
}
|
||||
}
|
@ -1,19 +1,26 @@
|
||||
import { BridgeConfigJira } from "../Config/Config";
|
||||
import { generateJiraURL } from "./AdminCommands";
|
||||
import { JiraOAuthResult } from "./Types";
|
||||
import { JiraOAuthResult, JiraProject } from "./Types";
|
||||
import { MessageQueue } from "../MessageQueue";
|
||||
import { OAuthRequest } from "../WebhookTypes";
|
||||
import { Router, Request, Response } from "express";
|
||||
import { Router, Request, Response, NextFunction } from "express";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import axios from "axios";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import { ApiError, ErrCode } from "../provisioning/api";
|
||||
import { HookshotJiraApi } from "./Client";
|
||||
|
||||
const log = new LogWrapper("JiraRouter");
|
||||
|
||||
interface OAuthQuery {
|
||||
state: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export class JiraWebhooksRouter {
|
||||
constructor(private readonly config: BridgeConfigJira, private readonly queue: MessageQueue) { }
|
||||
|
||||
private async onOAuth(req: Request, res: Response) {
|
||||
private async onOAuth(req: Request<unknown, unknown, unknown, OAuthQuery>, res: Response<string|{error: string}>) {
|
||||
if (typeof req.query.state !== "string") {
|
||||
return res.status(400).send({error: "Missing 'state' parameter"});
|
||||
}
|
||||
@ -64,12 +71,28 @@ export class JiraWebhooksRouter {
|
||||
}
|
||||
|
||||
|
||||
interface JiraAccountStatus {
|
||||
loggedIn: boolean;
|
||||
instances?: {
|
||||
name: string;
|
||||
url: string;
|
||||
id: string;
|
||||
}[]
|
||||
}
|
||||
interface JiraProjectsListing {
|
||||
name: string;
|
||||
key: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export class JiraProvisionerRouter {
|
||||
constructor(private readonly config: BridgeConfigJira, private readonly tokenStore: UserTokenStore) { }
|
||||
|
||||
public getRouter() {
|
||||
const router = Router();
|
||||
router.get("/oauth", this.onOAuth.bind(this));
|
||||
router.get("/account", this.onGetAccount.bind(this));
|
||||
router.get("/instances/:instanceName/projects", this.onGetInstanceProjects.bind(this));
|
||||
return router;
|
||||
}
|
||||
|
||||
@ -78,4 +101,66 @@ export class JiraProvisionerRouter {
|
||||
url: generateJiraURL(this.config.oauth.client_id, this.config.oauth.redirect_uri, this.tokenStore.createStateForOAuth(req.query.userId))
|
||||
});
|
||||
}
|
||||
|
||||
private async onGetAccount(req: Request<undefined, undefined, undefined, {userId: string}>, res: Response<JiraAccountStatus>, next: NextFunction) {
|
||||
const jiraUser = await this.tokenStore.getJiraForUser(req.query.userId);
|
||||
if (!jiraUser) {
|
||||
return res.send({
|
||||
loggedIn: false,
|
||||
});
|
||||
}
|
||||
const instances = [];
|
||||
try {
|
||||
for (const resource of await jiraUser.getAccessibleResources()) {
|
||||
instances.push({
|
||||
url: resource.url,
|
||||
name: resource.name,
|
||||
id: resource.id,
|
||||
});
|
||||
}
|
||||
} catch (ex) {
|
||||
log.warn(`Failed to fetch accessible resources for ${req.query.userId}`, ex);
|
||||
return next( new ApiError("Could not fetch accessible resources for JIRA user", ErrCode.Unknown));
|
||||
}
|
||||
return res.send({
|
||||
loggedIn: true,
|
||||
instances: instances
|
||||
})
|
||||
}
|
||||
|
||||
private async onGetInstanceProjects(req: Request<{instanceName: string}, undefined, undefined, {userId: string}>, res: Response<JiraProjectsListing[]>, next: NextFunction) {
|
||||
const jiraUser = await this.tokenStore.getJiraForUser(req.query.userId);
|
||||
if (!jiraUser) {
|
||||
// TODO: Better error?
|
||||
return next( new ApiError("Not logged in", ErrCode.ForbiddenUser));
|
||||
}
|
||||
|
||||
let resClient: HookshotJiraApi|null;
|
||||
try {
|
||||
resClient = await jiraUser.getClientForName(req.params.instanceName);
|
||||
} catch (ex) {
|
||||
log.warn(`Failed to fetch client for ${req.params.instanceName} for ${req.query.userId}`, ex);
|
||||
return next( new ApiError("Could not fetch accessible resources for JIRA user", ErrCode.Unknown));
|
||||
}
|
||||
if (!resClient) {
|
||||
return next( new ApiError("Instance not known or not accessible to this user", ErrCode.ForbiddenUser));
|
||||
}
|
||||
|
||||
const projects = [];
|
||||
try {
|
||||
for await (const project of resClient.getAllProjects()) {
|
||||
projects.push({
|
||||
key: project.key,
|
||||
name: project.name,
|
||||
// Technically not the real URL, but good enough for hookshot!
|
||||
url: `${resClient.resource.url}/projects/${project.key}`,
|
||||
});
|
||||
}
|
||||
} catch (ex) {
|
||||
log.warn(`Failed to fetch accessible projects for ${req.params.instanceName} / ${req.query.userId}`, ex);
|
||||
return next( new ApiError("Could not fetch accessible projects for JIRA user", ErrCode.Unknown));
|
||||
}
|
||||
|
||||
return res.send(projects);
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,10 @@ export enum ErrCode {
|
||||
* The secret token provided to the API was invalid or not given.
|
||||
*/
|
||||
BadToken = "HS_BAD_TOKEN",
|
||||
/**
|
||||
* The requested feature is not enabled in the bridge.
|
||||
*/
|
||||
DisabledFeature = "HS_DISABLED_FEATURE",
|
||||
}
|
||||
|
||||
const ErrCodeToStatusCode: Record<ErrCode, number> = {
|
||||
@ -58,6 +62,7 @@ const ErrCodeToStatusCode: Record<ErrCode, number> = {
|
||||
HS_NOT_IN_ROOM: 403,
|
||||
HS_BAD_VALUE: 400,
|
||||
HS_BAD_TOKEN: 401,
|
||||
HS_DISABLED_FEATURE: 500,
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
|
Loading…
x
Reference in New Issue
Block a user