Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7e4f43691 | |||
| c872ed559d |
@@ -0,0 +1,61 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- "**/*.md"
|
||||||
|
- "deploy/.env.example"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- "**/*.md"
|
||||||
|
- "deploy/.env.example"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
checks:
|
||||||
|
name: Check web and api
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10.28.2
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install web dependencies
|
||||||
|
run: pnpm install --frozen-lockfile --filter lanqin-email-web...
|
||||||
|
|
||||||
|
- name: Check shadcn/ui usage
|
||||||
|
run: pnpm --dir apps/web run check:shadcn
|
||||||
|
|
||||||
|
- name: Build web
|
||||||
|
run: pnpm --dir apps/web run build
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: apps/api/go.mod
|
||||||
|
cache-dependency-path: apps/api/go.sum
|
||||||
|
|
||||||
|
- name: Test api
|
||||||
|
working-directory: apps/api
|
||||||
|
run: go test ./...
|
||||||
@@ -1,21 +1,9 @@
|
|||||||
name: CI and Docker
|
name: Docker Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
paths-ignore:
|
|
||||||
- "**/*.md"
|
|
||||||
- "deploy/.env.example"
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths-ignore:
|
|
||||||
- "**/*.md"
|
|
||||||
- "deploy/.env.example"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -68,11 +56,31 @@ jobs:
|
|||||||
working-directory: apps/api
|
working-directory: apps/api
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Resolve release tag
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: checks
|
||||||
|
outputs:
|
||||||
|
tag: ${{ steps.release.outputs.tag }}
|
||||||
|
version: ${{ steps.release.outputs.version }}
|
||||||
|
release_url: ${{ steps.release.outputs.release_url }}
|
||||||
|
steps:
|
||||||
|
- name: Resolve tag version
|
||||||
|
id: release
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
tag="${GITHUB_REF_NAME}"
|
||||||
|
version="${tag#v}"
|
||||||
|
release_url="https://github.com/${GITHUB_REPOSITORY}/releases/tag/${tag}"
|
||||||
|
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_url=${release_url}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::notice::Docker release tag: ${tag}"
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
name: Build and publish ${{ matrix.name }}
|
name: Build and publish ${{ matrix.name }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: checks
|
needs: release
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -124,6 +132,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
image="${REGISTRY}/${GITHUB_REPOSITORY}${{ matrix.suffix }}"
|
image="${REGISTRY}/${GITHUB_REPOSITORY}${{ matrix.suffix }}"
|
||||||
echo "name=${image,,}" >> "$GITHUB_OUTPUT"
|
echo "name=${image,,}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${{ needs.release.outputs.tag }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=${{ needs.release.outputs.version }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Docker metadata
|
- name: Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
@@ -131,12 +141,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: ${{ steps.image.outputs.name }}
|
images: ${{ steps.image.outputs.name }}
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
type=raw,value=${{ steps.image.outputs.tag }}
|
||||||
type=ref,event=branch
|
type=raw,value=${{ steps.image.outputs.version }}
|
||||||
type=ref,event=tag
|
type=raw,value=latest
|
||||||
type=sha,prefix=sha-
|
type=sha,prefix=sha-
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.title=LanQin Email ${{ matrix.name }}
|
org.opencontainers.image.title=LanQin Email ${{ matrix.name }}
|
||||||
|
org.opencontainers.image.version=${{ steps.image.outputs.tag }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -145,5 +156,8 @@ jobs:
|
|||||||
file: ${{ matrix.file }}
|
file: ${{ matrix.file }}
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
|
build-args: |
|
||||||
|
VITE_APP_VERSION=${{ needs.release.outputs.tag }}
|
||||||
|
VITE_RELEASE_URL=${{ needs.release.outputs.release_url }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as React from "react"
|
|||||||
import DOMPurify from "dompurify"
|
import DOMPurify from "dompurify"
|
||||||
import { useSearchParams } from "react-router-dom"
|
import { useSearchParams } from "react-router-dom"
|
||||||
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { ArrowRight, CheckCircle2, Circle, Copy, Globe2, Mailbox, MoreHorizontal, Plus, RefreshCcw, Search, ShieldCheck, Trash2, Users } from "lucide-react"
|
import { ArrowRight, BookOpen, CheckCircle2, Circle, Copy, GitBranch, Github, Globe2, Mailbox, MoreHorizontal, Plus, RefreshCcw, Scale, Search, ShieldCheck, Star, Trash2, Users } from "lucide-react"
|
||||||
import { api, AdminUser, Alias, DNSRecord, Domain, Mailbox as MailboxType, MailMessage, MailTemplate, SystemSettings } from "@/lib/api"
|
import { api, AdminUser, Alias, DNSRecord, Domain, Mailbox as MailboxType, MailMessage, MailTemplate, SystemSettings } from "@/lib/api"
|
||||||
import { cn, decodeMimeHeader, formatBytes, formatDate } from "@/lib/utils"
|
import { cn, decodeMimeHeader, formatBytes, formatDate } from "@/lib/utils"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -35,6 +35,9 @@ const sectionLabels: Record<Section, string> = {
|
|||||||
settings: "系统设置",
|
settings: "系统设置",
|
||||||
}
|
}
|
||||||
const sectionKeys = Object.keys(sectionLabels) as Section[]
|
const sectionKeys = Object.keys(sectionLabels) as Section[]
|
||||||
|
const projectRepositoryUrl = "https://github.com/LanQin996/LanQin-Email"
|
||||||
|
const projectTag = import.meta.env.VITE_APP_VERSION || ""
|
||||||
|
const projectReleaseUrl = import.meta.env.VITE_RELEASE_URL || (projectTag ? `${projectRepositoryUrl}/releases/tag/${projectTag}` : "")
|
||||||
|
|
||||||
export function AdminPage() {
|
export function AdminPage() {
|
||||||
const overview = useQuery({ queryKey: ["admin", "overview"], queryFn: api.adminOverview })
|
const overview = useQuery({ queryKey: ["admin", "overview"], queryFn: api.adminOverview })
|
||||||
@@ -534,7 +537,7 @@ function SystemSettingsSection({ settings, domains }: { settings?: SystemSetting
|
|||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const templates = useQuery({ queryKey: ["admin", "mail-templates"], queryFn: api.mailTemplates })
|
const templates = useQuery({ queryKey: ["admin", "mail-templates"], queryFn: api.mailTemplates })
|
||||||
const [settingsTab, setSettingsTab] = React.useState<"base" | "smtp" | "storage" | "mail" | "templates" | "security">("base")
|
const [settingsTab, setSettingsTab] = React.useState<"base" | "smtp" | "storage" | "mail" | "templates" | "security" | "about">("base")
|
||||||
const [smtpRequireTls, setSmtpRequireTls] = React.useState(false)
|
const [smtpRequireTls, setSmtpRequireTls] = React.useState(false)
|
||||||
const [allowInsecureHttp, setAllowInsecureHttp] = React.useState(true)
|
const [allowInsecureHttp, setAllowInsecureHttp] = React.useState(true)
|
||||||
const [openRegistration, setOpenRegistration] = React.useState(false)
|
const [openRegistration, setOpenRegistration] = React.useState(false)
|
||||||
@@ -620,6 +623,7 @@ function SystemSettingsSection({ settings, domains }: { settings?: SystemSetting
|
|||||||
{ key: "mail", label: "邮件" },
|
{ key: "mail", label: "邮件" },
|
||||||
{ key: "templates", label: "模板" },
|
{ key: "templates", label: "模板" },
|
||||||
{ key: "security", label: "安全" },
|
{ key: "security", label: "安全" },
|
||||||
|
{ key: "about", label: "关于" },
|
||||||
]
|
]
|
||||||
return (
|
return (
|
||||||
<form key={formKey} onSubmit={(event) => { event.preventDefault(); save.mutate(new FormData(event.currentTarget)) }} className="space-y-6">
|
<form key={formKey} onSubmit={(event) => { event.preventDefault(); save.mutate(new FormData(event.currentTarget)) }} className="space-y-6">
|
||||||
@@ -736,13 +740,91 @@ function SystemSettingsSection({ settings, domains }: { settings?: SystemSetting
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>}
|
</Card>}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
{settingsTab === "about" && <AboutProjectCard />}
|
||||||
|
|
||||||
|
{settingsTab !== "about" && <div className="flex justify-end">
|
||||||
<Button disabled={save.isPending || !settings}>{save.isPending ? "保存中..." : "保存设置"}</Button>
|
<Button disabled={save.isPending || !settings}>{save.isPending ? "保存中..." : "保存设置"}</Button>
|
||||||
</div>
|
</div>}
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AboutProjectCard() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>关于</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4 text-sm">
|
||||||
|
<AboutRow label="版本">
|
||||||
|
{projectTag ? (
|
||||||
|
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
|
||||||
|
<a href={projectReleaseUrl} target="_blank" rel="noreferrer">
|
||||||
|
<GitBranch className="h-5 w-5 text-primary" />
|
||||||
|
{projectTag}
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" disabled>
|
||||||
|
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
||||||
|
未发布版本
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</AboutRow>
|
||||||
|
<AboutRow label="交流">
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
|
||||||
|
<a href={projectRepositoryUrl} target="_blank" rel="noreferrer">
|
||||||
|
<Github className="h-5 w-5" />
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
|
||||||
|
<a href={`${projectRepositoryUrl}/issues`} target="_blank" rel="noreferrer">
|
||||||
|
<Circle className="h-5 w-5 text-muted-foreground" />
|
||||||
|
Issues
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</AboutRow>
|
||||||
|
<AboutRow label="支持">
|
||||||
|
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
|
||||||
|
<a href={projectRepositoryUrl} target="_blank" rel="noreferrer">
|
||||||
|
<Star className="h-5 w-5 text-yellow-500" />
|
||||||
|
给项目点 Star
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</AboutRow>
|
||||||
|
<AboutRow label="帮助">
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
|
||||||
|
<a href={`${projectRepositoryUrl}#readme`} target="_blank" rel="noreferrer">
|
||||||
|
<BookOpen className="h-5 w-5 text-sky-500" />
|
||||||
|
项目文档
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
|
||||||
|
<a href={`${projectRepositoryUrl}/blob/main/LICENSE`} target="_blank" rel="noreferrer">
|
||||||
|
<Scale className="h-5 w-5 text-emerald-500" />
|
||||||
|
开源协议
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</AboutRow>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AboutRow({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2 sm:grid-cols-[4.5rem_minmax(0,1fr)] sm:items-center">
|
||||||
|
<div className="font-medium text-muted-foreground">{label}:</div>
|
||||||
|
<div className="min-w-0">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function TestSMTPDialog({ disabled }: { disabled?: boolean }) {
|
function TestSMTPDialog({ disabled }: { disabled?: boolean }) {
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
|
|||||||
Vendored
+9
@@ -1 +1,10 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_APP_VERSION?: string
|
||||||
|
readonly VITE_RELEASE_URL?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
|
|||||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||||
pnpm install --frozen-lockfile --filter lanqin-email-web...
|
pnpm install --frozen-lockfile --filter lanqin-email-web...
|
||||||
COPY apps/web apps/web
|
COPY apps/web apps/web
|
||||||
|
ARG VITE_APP_VERSION=""
|
||||||
|
ARG VITE_RELEASE_URL=""
|
||||||
RUN pnpm --dir apps/web run build
|
RUN pnpm --dir apps/web run build
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
|
|||||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||||
pnpm install --frozen-lockfile --filter lanqin-email-web...
|
pnpm install --frozen-lockfile --filter lanqin-email-web...
|
||||||
COPY apps/web apps/web
|
COPY apps/web apps/web
|
||||||
|
ARG VITE_APP_VERSION=""
|
||||||
|
ARG VITE_RELEASE_URL=""
|
||||||
RUN pnpm --dir apps/web run build
|
RUN pnpm --dir apps/web run build
|
||||||
|
|
||||||
FROM nginx:1.27-alpine
|
FROM nginx:1.27-alpine
|
||||||
|
|||||||
Reference in New Issue
Block a user