Skip to content

Commit 8dc41a2

Browse files
authored
Fix repo images in authed instance case and add manifest json (#332)
* wip fix repo images * fix config imports * add manifest json * more logos for manifest * add properly padded icon * support old gitlab token case, simplify getImage action, feedback * add changelog entry * fix build error
1 parent 397262e commit 8dc41a2

File tree

17 files changed

+216
-50
lines changed

17 files changed

+216
-50
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111
- Added seperate page for signup. [#311](https://github.com/sourcebot-dev/sourcebot/pull/331)
12+
- Fix repo images in authed instance case and add manifest json. [#332](https://github.com/sourcebot-dev/sourcebot/pull/332)
1213
- Added encryption logic for license keys. [#335](https://github.com/sourcebot-dev/sourcebot/pull/335)
1314

1415
## [4.1.1] - 2025-06-03

packages/backend/src/utils.ts

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { Logger } from "winston";
22
import { AppContext } from "./types.js";
33
import path from 'path';
44
import { PrismaClient, Repo } from "@sourcebot/db";
5-
import { decrypt } from "@sourcebot/crypto";
6-
import { Token } from "@sourcebot/schemas/v3/shared.type";
5+
import { getTokenFromConfig as getTokenFromConfigBase } from "@sourcebot/crypto";
76
import { BackendException, BackendError } from "@sourcebot/error";
87
import * as Sentry from "@sentry/node";
98

@@ -25,44 +24,21 @@ export const isRemotePath = (path: string) => {
2524
return path.startsWith('https://') || path.startsWith('http://');
2625
}
2726

28-
export const getTokenFromConfig = async (token: Token, orgId: number, db: PrismaClient, logger?: Logger) => {
29-
if ('secret' in token) {
30-
const secretKey = token.secret;
31-
const secret = await db.secret.findUnique({
32-
where: {
33-
orgId_key: {
34-
key: secretKey,
35-
orgId
36-
}
37-
}
38-
});
39-
40-
if (!secret) {
27+
export const getTokenFromConfig = async (token: any, orgId: number, db: PrismaClient, logger?: Logger) => {
28+
try {
29+
return await getTokenFromConfigBase(token, orgId, db);
30+
} catch (error: unknown) {
31+
if (error instanceof Error) {
4132
const e = new BackendException(BackendError.CONNECTION_SYNC_SECRET_DNE, {
42-
message: `Secret with key ${secretKey} not found for org ${orgId}`,
33+
message: error.message,
4334
});
4435
Sentry.captureException(e);
45-
logger?.error(e.metadata.message);
36+
logger?.error(error.message);
4637
throw e;
4738
}
48-
49-
const decryptedToken = decrypt(secret.iv, secret.encryptedValue);
50-
return decryptedToken;
51-
} else {
52-
const envToken = process.env[token.env];
53-
if (!envToken) {
54-
const e = new BackendException(BackendError.CONNECTION_SYNC_SECRET_DNE, {
55-
message: `Environment variable ${token.env} not found.`,
56-
});
57-
Sentry.captureException(e);
58-
logger?.error(e.metadata.message);
59-
throw e;
60-
}
61-
62-
return envToken;
39+
throw error;
6340
}
64-
}
65-
41+
};
6642

6743
export const resolvePathRelativeToConfig = (localPath: string, configPath: string) => {
6844
let absolutePath = localPath;

packages/crypto/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"postinstall": "yarn build"
99
},
1010
"dependencies": {
11+
"@sourcebot/db": "*",
12+
"@sourcebot/schemas": "*",
1113
"dotenv": "^16.4.5"
1214
},
1315
"devDependencies": {

packages/crypto/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export function verifySignature(data: string, signature: string, publicKeyPath:
7979
publicKey = fs.readFileSync(publicKeyPath, 'utf8');
8080
publicKeyCache.set(publicKeyPath, publicKey);
8181
}
82-
82+
8383
// Convert base64url signature to base64 if needed
8484
const base64Signature = signature.replace(/-/g, '+').replace(/_/g, '/');
8585
const paddedSignature = base64Signature + '='.repeat((4 - base64Signature.length % 4) % 4);
@@ -91,3 +91,5 @@ export function verifySignature(data: string, signature: string, publicKeyPath:
9191
return false;
9292
}
9393
}
94+
95+
export { getTokenFromConfig } from './tokenUtils.js';

packages/crypto/src/tokenUtils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { PrismaClient } from "@sourcebot/db";
2+
import { Token } from "@sourcebot/schemas/v3/shared.type";
3+
import { decrypt } from "./index.js";
4+
5+
export const getTokenFromConfig = async (token: Token, orgId: number, db: PrismaClient) => {
6+
if ('secret' in token) {
7+
const secretKey = token.secret;
8+
const secret = await db.secret.findUnique({
9+
where: {
10+
orgId_key: {
11+
key: secretKey,
12+
orgId
13+
}
14+
}
15+
});
16+
17+
if (!secret) {
18+
throw new Error(`Secret with key ${secretKey} not found for org ${orgId}`);
19+
}
20+
21+
const decryptedToken = decrypt(secret.iv, secret.encryptedValue);
22+
return decryptedToken;
23+
} else if ('env' in token) {
24+
const envToken = process.env[token.env];
25+
if (!envToken) {
26+
throw new Error(`Environment variable ${token.env} not found.`);
27+
}
28+
29+
return envToken;
30+
} else {
31+
throw new Error('Invalid token configuration');
32+
}
33+
};

packages/crypto/tsconfig.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"compilerOptions": {
3-
"target": "ES6",
4-
"module": "CommonJS",
5-
"lib": ["ES6"],
3+
"target": "ES2022",
4+
"module": "Node16",
5+
"lib": ["ES2023"],
66
"outDir": "dist",
77
"rootDir": "src",
88
"declaration": true,
@@ -11,11 +11,12 @@
1111
"strict": true,
1212
"noImplicitAny": true,
1313
"strictNullChecks": true,
14-
"moduleResolution": "node",
14+
"moduleResolution": "Node16",
1515
"esModuleInterop": true,
1616
"forceConsistentCasingInFileNames": true,
1717
"skipLibCheck": true,
18-
"isolatedModules": true
18+
"isolatedModules": true,
19+
"resolveJsonModule": true
1920
},
2021
"include": ["src/**/*"],
2122
"exclude": ["node_modules", "dist"]

packages/web/public/logo_512.png

18.1 KB
Loading

packages/web/public/manifest.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "Sourcebot",
3+
"short_name": "Sourcebot",
4+
"display": "standalone",
5+
"start_url": "/",
6+
"icons": [
7+
{
8+
"src": "/logo_512.png",
9+
"sizes": "512x512",
10+
"type": "image/png"
11+
}
12+
]
13+
}
14+

packages/web/src/actions.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import { CodeHostType, isServiceError } from "@/lib/utils";
77
import { prisma } from "@/prisma";
88
import { render } from "@react-email/components";
99
import * as Sentry from '@sentry/nextjs';
10-
import { decrypt, encrypt, generateApiKey, hashSecret } from "@sourcebot/crypto";
10+
import { decrypt, encrypt, generateApiKey, hashSecret, getTokenFromConfig } from "@sourcebot/crypto";
1111
import { ConnectionSyncStatus, OrgRole, Prisma, RepoIndexingStatus, StripeSubscriptionStatus, Org, ApiKey } from "@sourcebot/db";
1212
import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
1313
import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema";
1414
import { giteaSchema } from "@sourcebot/schemas/v3/gitea.schema";
1515
import { githubSchema } from "@sourcebot/schemas/v3/github.schema";
1616
import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
17+
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type";
18+
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
19+
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
1720
import Ajv from "ajv";
1821
import { StatusCodes } from "http-status-codes";
1922
import { cookies, headers } from "next/headers";
@@ -1712,6 +1715,76 @@ export const getSearchContexts = async (domain: string) => sew(() =>
17121715
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true
17131716
));
17141717

1718+
export const getRepoImage = async (repoId: number, domain: string): Promise<ArrayBuffer | ServiceError> => sew(async () => {
1719+
return await withAuth(async (userId) => {
1720+
return await withOrgMembership(userId, domain, async ({ org }) => {
1721+
const repo = await prisma.repo.findUnique({
1722+
where: {
1723+
id: repoId,
1724+
orgId: org.id,
1725+
},
1726+
include: {
1727+
connections: {
1728+
include: {
1729+
connection: true,
1730+
}
1731+
}
1732+
}
1733+
});
1734+
1735+
if (!repo || !repo.imageUrl) {
1736+
return notFound();
1737+
}
1738+
1739+
const authHeaders: Record<string, string> = {};
1740+
for (const { connection } of repo.connections) {
1741+
try {
1742+
if (connection.connectionType === 'github') {
1743+
const config = connection.config as unknown as GithubConnectionConfig;
1744+
if (config.token) {
1745+
const token = await getTokenFromConfig(config.token, connection.orgId, prisma);
1746+
authHeaders['Authorization'] = `token ${token}`;
1747+
break;
1748+
}
1749+
} else if (connection.connectionType === 'gitlab') {
1750+
const config = connection.config as unknown as GitlabConnectionConfig;
1751+
if (config.token) {
1752+
const token = await getTokenFromConfig(config.token, connection.orgId, prisma);
1753+
authHeaders['PRIVATE-TOKEN'] = token;
1754+
break;
1755+
}
1756+
} else if (connection.connectionType === 'gitea') {
1757+
const config = connection.config as unknown as GiteaConnectionConfig;
1758+
if (config.token) {
1759+
const token = await getTokenFromConfig(config.token, connection.orgId, prisma);
1760+
authHeaders['Authorization'] = `token ${token}`;
1761+
break;
1762+
}
1763+
}
1764+
} catch (error) {
1765+
logger.warn(`Failed to get token for connection ${connection.id}:`, error);
1766+
}
1767+
}
1768+
1769+
try {
1770+
const response = await fetch(repo.imageUrl, {
1771+
headers: authHeaders,
1772+
});
1773+
1774+
if (!response.ok) {
1775+
logger.warn(`Failed to fetch image from ${repo.imageUrl}: ${response.status}`);
1776+
return notFound();
1777+
}
1778+
1779+
const imageBuffer = await response.arrayBuffer();
1780+
return imageBuffer;
1781+
} catch (error) {
1782+
logger.error(`Error proxying image for repo ${repoId}:`, error);
1783+
return notFound();
1784+
}
1785+
}, /* minRequiredRole = */ OrgRole.GUEST);
1786+
}, /* allowSingleTenantUnauthedAccess = */ true);
1787+
});
17151788

17161789
////// Helpers ///////
17171790

packages/web/src/app/[domain]/connections/[id]/components/repoListItem.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { getDisplayTime } from "@/lib/utils";
3+
import { getDisplayTime, getRepoImageSrc } from "@/lib/utils";
44
import Image from "next/image";
55
import { StatusIcon } from "../../components/statusIcon";
66
import { RepoIndexingStatus } from "@sourcebot/db";
@@ -46,14 +46,16 @@ export const RepoListItem = ({
4646
}
4747
}, [status]);
4848

49+
const imageSrc = getRepoImageSrc(imageUrl, repoId, domain);
50+
4951
return (
5052
<div
5153
className="flex flex-row items-center p-4 border rounded-lg bg-background justify-between"
5254
>
5355
<div className="flex flex-row items-center gap-2">
54-
{imageUrl ? (
56+
{imageSrc ? (
5557
<Image
56-
src={imageUrl}
58+
src={imageSrc}
5759
alt={name}
5860
width={32}
5961
height={32}

packages/web/src/app/[domain]/repos/columns.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import { ArrowUpDown, ExternalLink, Clock, Loader2, CheckCircle2, XCircle, Trash
66
import Image from "next/image"
77
import { Badge } from "@/components/ui/badge"
88
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
9-
import { cn } from "@/lib/utils"
9+
import { cn, getRepoImageSrc } from "@/lib/utils"
1010
import { RepoIndexingStatus } from "@sourcebot/db";
1111
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
1212
import { AddRepoButton } from "./addRepoButton"
1313

1414
export type RepositoryColumnInfo = {
15+
repoId: number
1516
name: string
1617
imageUrl?: string
1718
connections: {
@@ -112,7 +113,7 @@ export const columns = (domain: string): ColumnDef<RepositoryColumnInfo>[] => [
112113
<div className="relative h-8 w-8 overflow-hidden rounded-md border bg-muted">
113114
{repo.imageUrl ? (
114115
<Image
115-
src={repo.imageUrl || "/placeholder.svg"}
116+
src={getRepoImageSrc(repo.imageUrl, repo.repoId, domain) || "/placeholder.svg"}
116117
alt={`${repo.name} logo`}
117118
width={32}
118119
height={32}

packages/web/src/app/[domain]/repos/repositoryTable.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const RepositoryTable = () => {
2525

2626
const tableRepos = useMemo(() => {
2727
if (reposLoading) return Array(4).fill(null).map(() => ({
28+
repoId: 0,
2829
name: "",
2930
connections: [],
3031
repoIndexingStatus: RepoIndexingStatus.NEW,
@@ -35,6 +36,7 @@ export const RepositoryTable = () => {
3536

3637
if (!repos) return [];
3738
return repos.map((repo): RepositoryColumnInfo => ({
39+
repoId: repo.repoId,
3840
name: repo.repoDisplayName ?? repo.repoName,
3941
imageUrl: repo.imageUrl,
4042
connections: repo.linkedConnections,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { getRepoImage } from "@/actions";
2+
import { isServiceError } from "@/lib/utils";
3+
import { NextRequest } from "next/server";
4+
5+
export async function GET(
6+
request: NextRequest,
7+
{ params }: { params: { domain: string; repoId: string } }
8+
) {
9+
const { domain, repoId } = params;
10+
const repoIdNum = parseInt(repoId);
11+
12+
if (isNaN(repoIdNum)) {
13+
return new Response("Invalid repo ID", { status: 400 });
14+
}
15+
16+
const result = await getRepoImage(repoIdNum, domain);
17+
if (isServiceError(result)) {
18+
return new Response(result.message, { status: result.statusCode });
19+
}
20+
21+
return new Response(result, {
22+
headers: {
23+
'Content-Type': 'image/png',
24+
'Cache-Control': 'public, max-age=3600',
25+
},
26+
});
27+
}

packages/web/src/app/layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { getEntitlements } from "@/features/entitlements/server";
1313
export const metadata: Metadata = {
1414
title: "Sourcebot",
1515
description: "Sourcebot",
16+
manifest: "/manifest.json",
1617
};
1718

1819
export default function RootLayout({

0 commit comments

Comments
 (0)