Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/ghcr-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Publish GHCR image

on:
push:
branches:
- main
workflow_dispatch:

jobs:
build:
name: Build and push image
runs-on: ubuntu-latest

permissions:
contents: read
packages: write

steps:
- name: Check out
uses: actions/checkout@v4
Comment on lines +19 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Set persist-credentials: false on the checkout step.

actions/checkout@v4 defaults to persist-credentials: true, which stores the GITHUB_TOKEN in .git/config on the runner. Since the Dockerfile copies the entire build context (COPY . .), these credentials could leak into the image if .git is not excluded by .dockerignore. Adding persist-credentials: false is a simple hardening measure since this workflow only needs to read the source, not push back to the repo.

🔒 Proposed fix
       - name: Check out
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Check out
uses: actions/checkout@v4
- name: Check out
uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 19-20: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ghcr-build.yml around lines 19 - 20, Add
persist-credentials: false to the actions/checkout@v4 step in the workflow’s
“Check out” step, ensuring checkout does not store the GITHUB_TOKEN in
.git/config before the Docker build.

Source: Linters/SAST tools


- name: Normalize GHCR repository
run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ env.GHCR_REPOSITORY }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=sha,prefix=sha-

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
sbom: false
2 changes: 1 addition & 1 deletion router/api-router.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func SetApiRouter(router *gin.Engine) {
//apiRouter.GET("/midjourney", controller.GetMidjourney)
apiRouter.GET("/home_page_content", controller.GetHomePageContent)
apiRouter.GET("/pricing", middleware.HeaderNavModuleAuth("pricing"), controller.GetPricing)
apiRouter.GET("/subscription/plans", controller.GetSubscriptionPlans)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the SubscriptionPlan model definition and its JSON tags
fd -t f 'subscription' model/ | head
rg -nP -C2 'type\s+SubscriptionPlan\s+struct' --type=go
# Show full struct with json tags to audit exposed fields
ast-grep run --pattern 'type SubscriptionPlan struct { $$$ }' --lang go
# Check NormalizeDefaults to confirm it does not populate sensitive data
rg -nP -C3 'func .*NormalizeDefaults' --type=go

Repository: QuantumNous/new-api

Length of output: 471


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- model/subscription.go (lines) ---'
wc -l model/subscription.go
cat -n model/subscription.go | sed -n '1,260p'

echo '--- search GetSubscriptionPlans / DTO ---'
rg -n -C 3 'GetSubscriptionPlans|SubscriptionPlanDTO|type .*SubscriptionPlan.*DTO|subscription/plans' controller router model

Repository: QuantumNous/new-api

Length of output: 12692


Hide internal plan metadata from the public plans endpoint

controller.GetSubscriptionPlans returns model.SubscriptionPlan directly, so unauthenticated callers receive fields like stripe_price_id, creem_product_id, waffo_pancake_product_id, upgrade_group, and downgrade_group. Map this to a public DTO and omit internal/provider-specific fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/api-router.go` at line 35, Update controller.GetSubscriptionPlans to
map each model.SubscriptionPlan to a dedicated public response DTO containing
only client-safe plan fields, excluding stripe_price_id, creem_product_id,
waffo_pancake_product_id, upgrade_group, and downgrade_group; return the DTO
collection from the /subscription/plans endpoint instead of exposing
model.SubscriptionPlan directly.

perfMetricsRoute := apiRouter.Group("/perf-metrics")
perfMetricsRoute.Use(middleware.HeaderNavModulePublicOrUserAuth("pricing"))
{
Expand Down Expand Up @@ -151,7 +152,6 @@ func SetApiRouter(router *gin.Engine) {
subscriptionRoute := apiRouter.Group("/subscription")
subscriptionRoute.Use(middleware.UserAuth())
{
subscriptionRoute.GET("/plans", controller.GetSubscriptionPlans)
subscriptionRoute.GET("/self", controller.GetSubscriptionSelf)
subscriptionRoute.PUT("/self/preference", controller.UpdateSubscriptionPreference)
subscriptionRoute.POST("/balance/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestBalancePay)
Expand Down
89 changes: 1 addition & 88 deletions web/default/src/components/config-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ import { useTheme } from '@/context/theme-provider'
import {
type ContentLayout,
THEME_PRESETS,
type ThemeFont,
type ThemePreset,
type ThemeRadius,
type ThemeScale,
Expand Down Expand Up @@ -107,7 +106,6 @@ export function ConfigDrawer() {
<div className={sideDrawerFormClassName()}>
<ThemeConfig />
<PresetConfig />
<FontConfig />
<RadiusConfig />
<ScaleConfig />
<SidebarConfig />
Expand Down Expand Up @@ -306,90 +304,6 @@ function PresetConfig() {
)
}

/**
* Font options shown in the theme drawer.
*
* Each option renders a live "Aa" preview in the font it represents.
* `Auto` deliberately leaves `fontFamily` undefined so the preview inherits
* the currently active body font — that way the user sees what `Auto` will
* actually look like for the active preset (Anthropic → serif glyphs,
* everything else → sans glyphs) without us having to duplicate the
* preset-default mapping in the UI.
*/
const FONT_OPTIONS: {
value: ThemeFont
label: string
// CSS font-family applied to the "Aa" preview. `undefined` = inherit
// from the current theme (used by the `default` option).
preview?: string
}[] = [
{ value: 'default', label: 'Auto', preview: undefined },
{ value: 'sans', label: 'Sans', preview: 'var(--font-sans)' },
{ value: 'serif', label: 'Serif', preview: 'var(--font-serif)' },
]

function FontConfig() {
const { t } = useTranslation()
const { defaults, customization, setFont } = useThemeCustomization()
return (
<div>
<SectionTitle
title={t('Font')}
showReset={customization.font !== defaults.font}
onReset={() => setFont(defaults.font)}
/>
<Radio
value={customization.font}
onValueChange={(v) => setFont(v as ThemeFont)}
className='grid w-full grid-cols-3 gap-4'
aria-label={t('Select body font')}
>
{FONT_OPTIONS.map((option) => (
<Item
key={option.value}
value={option.value}
className='group flex flex-col items-stretch outline-none'
aria-label={
option.value === 'default' ? t('System default') : option.label
}
>
<div
className={cn(
'ring-border relative h-12 rounded-md ring-[1px] transition',
'group-data-checked:ring-primary group-data-checked:shadow-md',
'group-focus-visible:ring-2',
'group-hover:ring-primary/60'
)}
>
<CircleCheck
className={cn(
'fill-primary absolute top-0 right-0 z-10 size-5 translate-x-1/2 -translate-y-1/2 stroke-white',
'group-data-unchecked:hidden'
)}
aria-hidden='true'
/>
<span
aria-hidden='true'
className='text-foreground absolute inset-0 flex items-center justify-center text-lg leading-none font-medium'
style={
option.preview
? { fontFamily: option.preview }
: // `font: inherit` defers to the active theme so the
// "Auto" tile previews what the resolved font will be.
{ font: 'inherit', fontSize: '1.125rem' }
}
>
Aa
</span>
</div>
<div className='mt-1.5 text-center text-xs'>{option.label}</div>
</Item>
))}
</Radio>
</div>
)
}

const RADIUS_OPTIONS: {
value: ThemeRadius
label: string
Expand Down Expand Up @@ -492,7 +406,6 @@ function ScaleConfig() {
{ value: 'sm', label: t('Compact'), rows: 4, rowGap: '3px' },
{ value: 'default', label: t('Default'), rows: 3, rowGap: '6px' },
{ value: 'lg', label: t('Comfortable'), rows: 2, rowGap: '10px' },
{ value: 'xl', label: t('Super Large'), rows: 1, rowGap: '14px' },
]
return (
<div>
Expand All @@ -504,7 +417,7 @@ function ScaleConfig() {
<Radio
value={customization.scale}
onValueChange={(v) => setScale(v as ThemeScale)}
className='grid w-full grid-cols-4 gap-3'
className='grid w-full grid-cols-3 gap-4'
aria-label={t('Select interface density')}
>
{scaleOptions.map((option) => (
Expand Down
40 changes: 5 additions & 35 deletions web/default/src/context/theme-customization-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,11 @@ import {
CONTENT_LAYOUT_VALUES,
type ContentLayout,
DEFAULT_THEME_CUSTOMIZATION,
resolveThemeFont,
THEME_COOKIE_KEYS,
THEME_FONT_VALUES,
THEME_PRESET_VALUES,
THEME_RADIUS_VALUES,
THEME_SCALE_VALUES,
type ThemeCustomization,
type ThemeFont,
type ThemePreset,
type ThemeRadius,
type ThemeScale,
Expand Down Expand Up @@ -69,7 +66,6 @@ type ThemeCustomizationContextType = {
defaults: ThemeCustomization
customization: ThemeCustomization
setPreset: (preset: ThemePreset) => void
setFont: (font: ThemeFont) => void
setRadius: (radius: ThemeRadius) => void
setScale: (scale: ThemeScale) => void
setContentLayout: (contentLayout: ContentLayout) => void
Expand All @@ -84,7 +80,6 @@ const FALLBACK_CONTEXT: ThemeCustomizationContextType = {
defaults: DEFAULT_THEME_CUSTOMIZATION,
customization: DEFAULT_THEME_CUSTOMIZATION,
setPreset: () => {},
setFont: () => {},
setRadius: () => {},
setScale: () => {},
setContentLayout: () => {},
Expand All @@ -104,13 +99,6 @@ export function ThemeCustomizationProvider(props: {
DEFAULT_THEME_CUSTOMIZATION.preset
)
)
const [font, _setFont] = useState<ThemeFont>(() =>
readCookie<ThemeFont>(
THEME_COOKIE_KEYS.font,
THEME_FONT_VALUES,
DEFAULT_THEME_CUSTOMIZATION.font
)
)
const [radius, _setRadius] = useState<ThemeRadius>(() =>
readCookie<ThemeRadius>(
THEME_COOKIE_KEYS.radius,
Expand Down Expand Up @@ -142,15 +130,10 @@ export function ThemeCustomizationProvider(props: {
)
}, [preset])

// Font is the one axis where we resolve before writing the attribute:
// the persisted preference may be `default`, but CSS works in terms of
// the concrete `sans`/`serif` choice that should drive the cascade.
// Resolving here (instead of in CSS via `:not()` selectors) keeps the
// stylesheet to one simple `[data-theme-font='serif']` selector and lets
// future presets opt into typography via `PRESET_DEFAULT_FONT` alone.
useEffect(() => {
applyAttribute('data-theme-font', resolveThemeFont(font, preset))
}, [font, preset])
applyAttribute('data-theme-font', null)
removeCookie('theme_font')
}, [])

useEffect(() => {
applyAttribute(
Expand Down Expand Up @@ -179,15 +162,6 @@ export function ThemeCustomizationProvider(props: {
}
}, [])

const setFont = useCallback((value: ThemeFont) => {
_setFont(value)
if (value === DEFAULT_THEME_CUSTOMIZATION.font) {
removeCookie(THEME_COOKIE_KEYS.font)
} else {
setCookie(THEME_COOKIE_KEYS.font, value, COOKIE_MAX_AGE)
}
}, [])

const setRadius = useCallback((value: ThemeRadius) => {
_setRadius(value)
if (value === DEFAULT_THEME_CUSTOMIZATION.radius) {
Expand Down Expand Up @@ -217,31 +191,27 @@ export function ThemeCustomizationProvider(props: {

const resetCustomization = useCallback(() => {
setPreset(DEFAULT_THEME_CUSTOMIZATION.preset)
setFont(DEFAULT_THEME_CUSTOMIZATION.font)
setRadius(DEFAULT_THEME_CUSTOMIZATION.radius)
setScale(DEFAULT_THEME_CUSTOMIZATION.scale)
setContentLayout(DEFAULT_THEME_CUSTOMIZATION.contentLayout)
}, [setPreset, setFont, setRadius, setScale, setContentLayout])
}, [setPreset, setRadius, setScale, setContentLayout])

const value = useMemo<ThemeCustomizationContextType>(
() => ({
defaults: DEFAULT_THEME_CUSTOMIZATION,
customization: { preset, font, radius, scale, contentLayout },
customization: { preset, radius, scale, contentLayout },
setPreset,
setFont,
setRadius,
setScale,
setContentLayout,
resetCustomization,
}),
[
preset,
font,
radius,
scale,
contentLayout,
setPreset,
setFont,
setRadius,
setScale,
setContentLayout,
Expand Down
1 change: 0 additions & 1 deletion web/default/src/features/home/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { api } from '@/lib/api'

import type { HomePageContentResponse } from './types'

// ============================================================================
Expand Down
2 changes: 0 additions & 2 deletions web/default/src/features/home/components/gateway-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useTranslation } from 'react-i18next'

import { Separator } from '@/components/ui/separator'

import { getGatewayFeatures } from '../constants'

interface GatewayCardProps {
Expand Down
1 change: 0 additions & 1 deletion web/default/src/features/home/components/hero-buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ For commercial licensing, please contact support@quantumnous.com
import { Link } from '@tanstack/react-router'
import { ArrowRight } from 'lucide-react'
import { useTranslation } from 'react-i18next'

import { Button } from '@/components/ui/button'

interface HeroButtonsProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useState, useEffect, useRef, type ReactNode } from 'react'

import { cn } from '@/lib/utils'

type AccentTone = 'emerald' | 'amber' | 'blue' | 'violet'
Expand Down Expand Up @@ -164,11 +163,7 @@ const API_DEMOS: ApiDemoConfig[] = [
const CYCLE_INTERVAL = 4500
const TRANSITION_MS = 220

interface HeroTerminalDemoProps {
className?: string
}

export function HeroTerminalDemo(props: HeroTerminalDemoProps) {
export function HeroTerminalDemo() {
const [activeIndex, setActiveIndex] = useState(0)
const [transitioning, setTransitioning] = useState(false)
const intervalRef = useRef<ReturnType<typeof setInterval>>(undefined)
Expand Down Expand Up @@ -207,7 +202,7 @@ export function HeroTerminalDemo(props: HeroTerminalDemoProps) {
const accent = ACCENT_CLASSES[demo.accent]

return (
<div className={cn('mx-auto w-full max-w-2xl', props.className)}>
<div className='mx-auto mt-16 w-full max-w-2xl'>
<div
className={cn(
'overflow-hidden rounded-2xl border backdrop-blur-sm',
Expand Down
1 change: 1 addition & 0 deletions web/default/src/features/home/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ export { CTA } from './sections/cta'
export { Features } from './sections/features'
export { Hero } from './sections/hero'
export { HowItWorks } from './sections/how-it-works'
export { ModelPricing } from './sections/model-pricing'
export { Stats } from './sections/stats'
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { cn } from '@/lib/utils'

import { IconCard } from './icon-card'

interface ScrollingIconsProps {
Expand Down
3 changes: 1 addition & 2 deletions web/default/src/features/home/components/sections/cta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ For commercial licensing, please contact support@quantumnous.com
import { Link } from '@tanstack/react-router'
import { ArrowRight } from 'lucide-react'
import { useTranslation } from 'react-i18next'

import { AnimateInView } from '@/components/animate-in-view'
import { Button } from '@/components/ui/button'
import { AnimateInView } from '@/components/animate-in-view'

interface CTAProps {
className?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
HeartHandshake,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'

import { AnimateInView } from '@/components/animate-in-view'

interface FeaturesProps {
Expand Down
Loading
Loading