mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
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
This commit is contained in:
parent
397262ecf7
commit
8dc41a22b9
17 changed files with 216 additions and 50 deletions
|
|
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
- Added seperate page for signup. [#311](https://github.com/sourcebot-dev/sourcebot/pull/331)
|
||||
- Fix repo images in authed instance case and add manifest json. [#332](https://github.com/sourcebot-dev/sourcebot/pull/332)
|
||||
- Added encryption logic for license keys. [#335](https://github.com/sourcebot-dev/sourcebot/pull/335)
|
||||
|
||||
## [4.1.1] - 2025-06-03
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import { Logger } from "winston";
|
|||
import { AppContext } from "./types.js";
|
||||
import path from 'path';
|
||||
import { PrismaClient, Repo } from "@sourcebot/db";
|
||||
import { decrypt } from "@sourcebot/crypto";
|
||||
import { Token } from "@sourcebot/schemas/v3/shared.type";
|
||||
import { getTokenFromConfig as getTokenFromConfigBase } from "@sourcebot/crypto";
|
||||
import { BackendException, BackendError } from "@sourcebot/error";
|
||||
import * as Sentry from "@sentry/node";
|
||||
|
||||
|
|
@ -25,44 +24,21 @@ export const isRemotePath = (path: string) => {
|
|||
return path.startsWith('https://') || path.startsWith('http://');
|
||||
}
|
||||
|
||||
export const getTokenFromConfig = async (token: Token, orgId: number, db: PrismaClient, logger?: Logger) => {
|
||||
if ('secret' in token) {
|
||||
const secretKey = token.secret;
|
||||
const secret = await db.secret.findUnique({
|
||||
where: {
|
||||
orgId_key: {
|
||||
key: secretKey,
|
||||
orgId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!secret) {
|
||||
export const getTokenFromConfig = async (token: any, orgId: number, db: PrismaClient, logger?: Logger) => {
|
||||
try {
|
||||
return await getTokenFromConfigBase(token, orgId, db);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
const e = new BackendException(BackendError.CONNECTION_SYNC_SECRET_DNE, {
|
||||
message: `Secret with key ${secretKey} not found for org ${orgId}`,
|
||||
message: error.message,
|
||||
});
|
||||
Sentry.captureException(e);
|
||||
logger?.error(e.metadata.message);
|
||||
logger?.error(error.message);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const decryptedToken = decrypt(secret.iv, secret.encryptedValue);
|
||||
return decryptedToken;
|
||||
} else {
|
||||
const envToken = process.env[token.env];
|
||||
if (!envToken) {
|
||||
const e = new BackendException(BackendError.CONNECTION_SYNC_SECRET_DNE, {
|
||||
message: `Environment variable ${token.env} not found.`,
|
||||
});
|
||||
Sentry.captureException(e);
|
||||
logger?.error(e.metadata.message);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return envToken;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export const resolvePathRelativeToConfig = (localPath: string, configPath: string) => {
|
||||
let absolutePath = localPath;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
"postinstall": "yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sourcebot/db": "*",
|
||||
"@sourcebot/schemas": "*",
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -91,3 +91,5 @@ export function verifySignature(data: string, signature: string, publicKeyPath:
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { getTokenFromConfig } from './tokenUtils.js';
|
||||
33
packages/crypto/src/tokenUtils.ts
Normal file
33
packages/crypto/src/tokenUtils.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { PrismaClient } from "@sourcebot/db";
|
||||
import { Token } from "@sourcebot/schemas/v3/shared.type";
|
||||
import { decrypt } from "./index.js";
|
||||
|
||||
export const getTokenFromConfig = async (token: Token, orgId: number, db: PrismaClient) => {
|
||||
if ('secret' in token) {
|
||||
const secretKey = token.secret;
|
||||
const secret = await db.secret.findUnique({
|
||||
where: {
|
||||
orgId_key: {
|
||||
key: secretKey,
|
||||
orgId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!secret) {
|
||||
throw new Error(`Secret with key ${secretKey} not found for org ${orgId}`);
|
||||
}
|
||||
|
||||
const decryptedToken = decrypt(secret.iv, secret.encryptedValue);
|
||||
return decryptedToken;
|
||||
} else if ('env' in token) {
|
||||
const envToken = process.env[token.env];
|
||||
if (!envToken) {
|
||||
throw new Error(`Environment variable ${token.env} not found.`);
|
||||
}
|
||||
|
||||
return envToken;
|
||||
} else {
|
||||
throw new Error('Invalid token configuration');
|
||||
}
|
||||
};
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "CommonJS",
|
||||
"lib": ["ES6"],
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"lib": ["ES2023"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
|
|
@ -11,11 +11,12 @@
|
|||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "Node16",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
|
|
|||
BIN
packages/web/public/logo_512.png
Normal file
BIN
packages/web/public/logo_512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
14
packages/web/public/manifest.json
Normal file
14
packages/web/public/manifest.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "Sourcebot",
|
||||
"short_name": "Sourcebot",
|
||||
"display": "standalone",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/logo_512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -7,13 +7,16 @@ import { CodeHostType, isServiceError } from "@/lib/utils";
|
|||
import { prisma } from "@/prisma";
|
||||
import { render } from "@react-email/components";
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { decrypt, encrypt, generateApiKey, hashSecret } from "@sourcebot/crypto";
|
||||
import { decrypt, encrypt, generateApiKey, hashSecret, getTokenFromConfig } from "@sourcebot/crypto";
|
||||
import { ConnectionSyncStatus, OrgRole, Prisma, RepoIndexingStatus, StripeSubscriptionStatus, Org, ApiKey } from "@sourcebot/db";
|
||||
import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
|
||||
import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema";
|
||||
import { giteaSchema } from "@sourcebot/schemas/v3/gitea.schema";
|
||||
import { githubSchema } from "@sourcebot/schemas/v3/github.schema";
|
||||
import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
|
||||
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type";
|
||||
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
|
||||
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
|
||||
import Ajv from "ajv";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
|
@ -1712,6 +1715,76 @@ export const getSearchContexts = async (domain: string) => sew(() =>
|
|||
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true
|
||||
));
|
||||
|
||||
export const getRepoImage = async (repoId: number, domain: string): Promise<ArrayBuffer | ServiceError> => sew(async () => {
|
||||
return await withAuth(async (userId) => {
|
||||
return await withOrgMembership(userId, domain, async ({ org }) => {
|
||||
const repo = await prisma.repo.findUnique({
|
||||
where: {
|
||||
id: repoId,
|
||||
orgId: org.id,
|
||||
},
|
||||
include: {
|
||||
connections: {
|
||||
include: {
|
||||
connection: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!repo || !repo.imageUrl) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const authHeaders: Record<string, string> = {};
|
||||
for (const { connection } of repo.connections) {
|
||||
try {
|
||||
if (connection.connectionType === 'github') {
|
||||
const config = connection.config as unknown as GithubConnectionConfig;
|
||||
if (config.token) {
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, prisma);
|
||||
authHeaders['Authorization'] = `token ${token}`;
|
||||
break;
|
||||
}
|
||||
} else if (connection.connectionType === 'gitlab') {
|
||||
const config = connection.config as unknown as GitlabConnectionConfig;
|
||||
if (config.token) {
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, prisma);
|
||||
authHeaders['PRIVATE-TOKEN'] = token;
|
||||
break;
|
||||
}
|
||||
} else if (connection.connectionType === 'gitea') {
|
||||
const config = connection.config as unknown as GiteaConnectionConfig;
|
||||
if (config.token) {
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, prisma);
|
||||
authHeaders['Authorization'] = `token ${token}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to get token for connection ${connection.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(repo.imageUrl, {
|
||||
headers: authHeaders,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn(`Failed to fetch image from ${repo.imageUrl}: ${response.status}`);
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const imageBuffer = await response.arrayBuffer();
|
||||
return imageBuffer;
|
||||
} catch (error) {
|
||||
logger.error(`Error proxying image for repo ${repoId}:`, error);
|
||||
return notFound();
|
||||
}
|
||||
}, /* minRequiredRole = */ OrgRole.GUEST);
|
||||
}, /* allowSingleTenantUnauthedAccess = */ true);
|
||||
});
|
||||
|
||||
////// Helpers ///////
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { getDisplayTime } from "@/lib/utils";
|
||||
import { getDisplayTime, getRepoImageSrc } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { StatusIcon } from "../../components/statusIcon";
|
||||
import { RepoIndexingStatus } from "@sourcebot/db";
|
||||
|
|
@ -46,14 +46,16 @@ export const RepoListItem = ({
|
|||
}
|
||||
}, [status]);
|
||||
|
||||
const imageSrc = getRepoImageSrc(imageUrl, repoId, domain);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row items-center p-4 border rounded-lg bg-background justify-between"
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{imageUrl ? (
|
||||
{imageSrc ? (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
src={imageSrc}
|
||||
alt={name}
|
||||
width={32}
|
||||
height={32}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import { ArrowUpDown, ExternalLink, Clock, Loader2, CheckCircle2, XCircle, Trash
|
|||
import Image from "next/image"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn, getRepoImageSrc } from "@/lib/utils"
|
||||
import { RepoIndexingStatus } from "@sourcebot/db";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { AddRepoButton } from "./addRepoButton"
|
||||
|
||||
export type RepositoryColumnInfo = {
|
||||
repoId: number
|
||||
name: string
|
||||
imageUrl?: string
|
||||
connections: {
|
||||
|
|
@ -112,7 +113,7 @@ export const columns = (domain: string): ColumnDef<RepositoryColumnInfo>[] => [
|
|||
<div className="relative h-8 w-8 overflow-hidden rounded-md border bg-muted">
|
||||
{repo.imageUrl ? (
|
||||
<Image
|
||||
src={repo.imageUrl || "/placeholder.svg"}
|
||||
src={getRepoImageSrc(repo.imageUrl, repo.repoId, domain) || "/placeholder.svg"}
|
||||
alt={`${repo.name} logo`}
|
||||
width={32}
|
||||
height={32}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export const RepositoryTable = () => {
|
|||
|
||||
const tableRepos = useMemo(() => {
|
||||
if (reposLoading) return Array(4).fill(null).map(() => ({
|
||||
repoId: 0,
|
||||
name: "",
|
||||
connections: [],
|
||||
repoIndexingStatus: RepoIndexingStatus.NEW,
|
||||
|
|
@ -35,6 +36,7 @@ export const RepositoryTable = () => {
|
|||
|
||||
if (!repos) return [];
|
||||
return repos.map((repo): RepositoryColumnInfo => ({
|
||||
repoId: repo.repoId,
|
||||
name: repo.repoDisplayName ?? repo.repoName,
|
||||
imageUrl: repo.imageUrl,
|
||||
connections: repo.linkedConnections,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { getRepoImage } from "@/actions";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { domain: string; repoId: string } }
|
||||
) {
|
||||
const { domain, repoId } = params;
|
||||
const repoIdNum = parseInt(repoId);
|
||||
|
||||
if (isNaN(repoIdNum)) {
|
||||
return new Response("Invalid repo ID", { status: 400 });
|
||||
}
|
||||
|
||||
const result = await getRepoImage(repoIdNum, domain);
|
||||
if (isServiceError(result)) {
|
||||
return new Response(result.message, { status: result.statusCode });
|
||||
}
|
||||
|
||||
return new Response(result, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import { getEntitlements } from "@/features/entitlements/server";
|
|||
export const metadata: Metadata = {
|
||||
title: "Sourcebot",
|
||||
description: "Sourcebot",
|
||||
manifest: "/manifest.json",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
|
|
|||
|
|
@ -409,3 +409,32 @@ export const requiredQueryParamGuard = (request: NextRequest, param: string): Se
|
|||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const getRepoImageSrc = (imageUrl: string | undefined, repoId: number, domain: string): string | undefined => {
|
||||
if (!imageUrl) return undefined;
|
||||
|
||||
try {
|
||||
const url = new URL(imageUrl);
|
||||
|
||||
// List of known public instances that don't require authentication
|
||||
const publicHostnames = [
|
||||
'github.com',
|
||||
'gitlab.com',
|
||||
'avatars.githubusercontent.com',
|
||||
'gitea.com',
|
||||
'bitbucket.org',
|
||||
];
|
||||
|
||||
const isPublicInstance = publicHostnames.includes(url.hostname);
|
||||
|
||||
if (isPublicInstance) {
|
||||
return imageUrl;
|
||||
} else {
|
||||
// Use the proxied route for self-hosted instances
|
||||
return `/api/${domain}/repos/${repoId}/image`;
|
||||
}
|
||||
} catch {
|
||||
// If URL parsing fails, use the original URL
|
||||
return imageUrl;
|
||||
}
|
||||
};
|
||||
|
|
@ -33,6 +33,6 @@ export async function middleware(request: NextRequest) {
|
|||
export const config = {
|
||||
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
|
||||
matcher: [
|
||||
'/((?!api|_next/static|ingest|_next/image|favicon.ico|sitemap.xml|robots.txt|sb_logo_light_large.png|arrow.png|placeholder_avatar.png).*)',
|
||||
'/((?!api|_next/static|ingest|_next/image|favicon.ico|sitemap.xml|robots.txt|manifest.json|logo_192.png|logo_512.png|sb_logo_light_large.png|arrow.png|placeholder_avatar.png).*)',
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5803,13 +5803,15 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "@sourcebot/crypto@workspace:packages/crypto"
|
||||
dependencies:
|
||||
"@sourcebot/db": "npm:*"
|
||||
"@sourcebot/schemas": "npm:*"
|
||||
"@types/node": "npm:^22.7.5"
|
||||
dotenv: "npm:^16.4.5"
|
||||
typescript: "npm:^5.7.3"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@sourcebot/db@workspace:*, @sourcebot/db@workspace:packages/db":
|
||||
"@sourcebot/db@npm:*, @sourcebot/db@workspace:*, @sourcebot/db@workspace:packages/db":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@sourcebot/db@workspace:packages/db"
|
||||
dependencies:
|
||||
|
|
@ -5869,7 +5871,7 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@sourcebot/schemas@workspace:*, @sourcebot/schemas@workspace:packages/schemas":
|
||||
"@sourcebot/schemas@npm:*, @sourcebot/schemas@workspace:*, @sourcebot/schemas@workspace:packages/schemas":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@sourcebot/schemas@workspace:packages/schemas"
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Reference in a new issue