41 Commits

Author SHA1 Message Date
oiov
cc4c6c5e96 fix(docker): prisma client package 2025-05-27 11:11:00 +08:00
oiov
3301570213 fix(docker): install prisma on runner 2025-05-27 11:01:52 +08:00
oiov
c65176e607 fix(docker): check db do not run 2025-05-27 10:50:07 +08:00
oiov
55aa93d117 feats(docker): add check db script 2025-05-27 10:06:49 +08:00
oiov
7c61b7fc44 docs(deploy): add deploy methods 2025-05-26 21:56:47 +08:00
oiov
bc7f86119c fixup type error 2025-05-26 21:39:01 +08:00
oiov
bc1490f0fd fixup 2025-05-26 21:25:54 +08:00
oiov
7bf2aa8b3c test 2025-05-26 20:43:56 +08:00
oiov
ba086b602f test 2025-05-26 20:19:04 +08:00
oiov
0d793ee31c test 2025-05-26 19:59:43 +08:00
oiov
7579be007f test 2025-05-26 19:53:02 +08:00
oiov
515e7d2719 fixup error 2025-05-26 18:05:23 +08:00
oiov
c9cfdfc07a upd 2025-05-26 17:59:28 +08:00
oiov
c589afd859 fixup url error 2025-05-26 17:50:39 +08:00
oiov
f10f8af0f6 chore 2025-05-26 17:42:37 +08:00
oiov
cbeba449ef chore: get geo and ua info without vercel 2025-05-26 17:40:01 +08:00
oiov
fa02ca000b test 2025-05-26 16:41:50 +08:00
oiov
af01d60d9b fixup 2025-05-26 16:36:36 +08:00
oiov
06f06a8a52 test 2025-05-26 16:16:48 +08:00
oiov
fc54d9e176 fixup 2025-05-26 16:06:30 +08:00
oiov
8fab48f849 test 2025-05-26 15:57:34 +08:00
oiov
69878126f6 test geo 2025-05-26 14:40:26 +08:00
oiov
0185520445 upd 2025-05-25 22:34:12 +08:00
oiov
00cb224e84 test 2025-05-25 22:20:18 +08:00
oiov
c5a932b9f1 prisma/prisma/discussions/19341 2025-05-25 22:09:44 +08:00
oiov
becc328811 emmm 2025-05-25 21:45:37 +08:00
oiov
c2ae4c78f7 fixup 2025-05-25 21:32:04 +08:00
oiov
40f2483332 chore 2025-05-25 21:15:39 +08:00
oiov
a27eb84d61 upd 2025-05-25 21:02:14 +08:00
oiov
01b80eaf9e test 2025-05-25 20:59:10 +08:00
oiov
1e713ea613 test 2025-05-25 17:16:24 +08:00
oiov
1eb7c71ff9 test 2025-05-25 17:01:06 +08:00
oiov
b9bf2733f9 change workflow 2025-05-25 15:27:34 +08:00
oiov
1e48c209f7 chore 2025-05-25 15:20:13 +08:00
oiov
fff455312e chore 2025-05-25 15:15:45 +08:00
oiov
a5626ebefe chore 2025-05-25 15:07:33 +08:00
oiov
400b1aac8d test docker build 2025-05-25 15:02:16 +08:00
oiov
872baa7933 chore 2025-05-25 12:07:52 +08:00
oiov
04b47b62ad fix(docker): change github star api 2025-05-25 11:56:17 +08:00
oiov
8894d2daae fix(docker): build error 2025-05-25 11:17:38 +08:00
oiov
3145ef884d feats: deploy with docker 2025-05-25 10:57:26 +08:00
36 changed files with 926 additions and 211 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
# .dockerignore
# node_modules
# npm-debug.log
# README.md
# .env*
# .next
# .git

View File

@@ -4,9 +4,10 @@
NEXT_PUBLIC_APP_URL=http://localhost:3000
# -----------------------------------------------------------------------------
# Authentication (NextAuth.js)
# Authentication (NextAuth.js 5.0.x)
# -----------------------------------------------------------------------------
AUTH_SECRET=
AUTH_SECRET=abc123
AUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
@@ -37,5 +38,6 @@ SCREENSHOTONE_BASE_URL=https://shot.wr.do
# GitHub api token for getting gitHub stars count
GITHUB_TOKEN=
# Skip DB check and migration (only for docker), default is true. if false, will check and migrate database each time start docker compose.
SKIP_DB_CHECK=true
SKIP_DB_MIGRATION=true

64
.github/workflows/docker-build-push.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Build and Push Docker Image to GHCR
on:
push:
branches:
- main
tags:
- 'v*.*.*'
pull_request:
branches:
- main
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}/wrdo
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
# 检出代码
- name: Checkout repository
uses: actions/checkout@v4
# 设置 Docker Buildx支持多平台构建
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# 登录到 GitHub Container Registry
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 提取 Docker 镜像元数据(标签、版本等)
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,format=short
type=ref,event=branch,prefix=
type=ref,event=tag
# 构建并推送 Docker 镜像
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true # ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
ENVIRONMENT=${{ github.event.inputs.environment || 'production' }}
cache-from: type=gha
cache-to: type=gha,mode=max

60
Dockerfile Normal file
View File

@@ -0,0 +1,60 @@
FROM node:20-alpine AS base
FROM base AS deps
RUN apk add --no-cache openssl
RUN apk add --no-cache libc6-compat
WORKDIR /app
RUN npm install -g pnpm
COPY . .
RUN pnpm config set registry https://registry.npmmirror.com
RUN pnpm i --frozen-lockfile
FROM base AS builder
WORKDIR /app
RUN apk add --no-cache openssl
RUN npm install -g pnpm
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm run build
FROM base AS runner
WORKDIR /app
RUN apk add --no-cache openssl
RUN npm install -g pnpm
ENV NODE_ENV=production
ENV IS_DOCKER=true
RUN pnpm add npm-run-all dotenv prisma@5.17.0 @prisma/client@5.17.0
COPY --from=builder /app/public ./public
COPY --from=builder /app/prisma ./prisma
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Check db
COPY scripts/check-db.js /app/scripts/check-db.js
EXPOSE 3000
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
# CMD ["node", "server.js"]
CMD ["pnpm", "start-docker"]

View File

@@ -14,24 +14,27 @@
- <20>😀 **权限管理**:方便审核的管理员面板
- 🔒 **安全可靠**:基于 Cloudflare 强大的 DNS API
## Screenshots
## 截图预览
![screenshot](https://wr.do/_static/images/light-preview.png)
![screenshot](https://wr.do/_static/images/example_01.png)
![screenshot](https://wr.do/_static/images/example_02.png)
![screenshot](https://wr.do/_static/images/example_03.png)
<table>
<tr>
<td><img src="https://wr.do/_static/images/light-preview.png" /></td>
<td><img src="https://wr.do/_static/images/example_02.png" /></td>
</tr>
<tr>
<td><img src="https://wr.do/_static/images/example_01.png" /></td>
<td><img src="https://wr.do/_static/images/realtime-globe.png" /></td>
</tr>
<tr>
<td><img src="https://wr.do/_static/images/example_03.png" /></td>
<td><img src="https://wr.do/_static/images/domains.png" /></td>
</tr>
</table>
## 快速开始
查看开发者[快速开始](https://wr.do/docs/developer/quick-start)的详细文档。
查看有关[快速开始](https://wr.do/docs/quick-start)的文档。
## 自托管教程
### 要求
- [Vercel](https://vercel.com) 账户用于部署应用
@@ -43,6 +46,23 @@
查看 [email worker](https://wr.do/docs/developer/cloudflare-email-worker) 文档用于邮件接收。
## 自部署教程
### 使用 Vercel 部署
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/oiov/wr.do.git&project-name=wrdo&env=DATABASE_URL&env=AUTH_SECRET&env=RESEND_API_KEY&env=NEXT_PUBLIC_EMAIL_R2_DOMAIN&env=NEXT_PUBLIC_OPEN_SIGNUP&env=GITHUB_TOKEN)
记得填写必要的环境变量。
### 使用 Docker Compose 部署
在服务器中创建一个文件夹,进入该文件夹并新建`docker-compose.yml`文件,填写必要的环境变量,然后执行:
```bash
docker compose up -d
```
## 本地开发
`.env.example` 复制为 `.env` 并填写必要的环境变量。
@@ -63,7 +83,7 @@ pnpm postinstall
pnpm db:push
```
#### 激活管理员面板
#### 管理员初始化
Follow https://localhost:3000/setup

View File

@@ -17,20 +17,24 @@
## Screenshots
![screenshot](https://wr.do/_static/images/light-preview.png)
<table>
<tr>
<td><img src="https://wr.do/_static/images/light-preview.png" /></td>
<td><img src="https://wr.do/_static/images/example_02.png" /></td>
</tr>
<tr>
<td><img src="https://wr.do/_static/images/example_01.png" /></td>
<td><img src="https://wr.do/_static/images/realtime-globe.png" /></td>
</tr>
<tr>
<td><img src="https://wr.do/_static/images/example_03.png" /></td>
<td><img src="https://wr.do/_static/images/domains.png" /></td>
</tr>
</table>
![screenshot](https://wr.do/_static/images/example_02.png)
![screenshot](https://wr.do/_static/images/example_01.png)
![screenshot](https://wr.do/_static/images/example_03.png)
## Quick Start
See usage docs about [guide](https://wr.do/docs/quick-start) for quick start.
## Self-hosted Tutorial
See step by step installation tutorial at [Quick Start for Developer](https://wr.do/docs/developer/quick-start).
### Requirements
@@ -44,15 +48,41 @@ See more docs about [developer](https://wr.do/docs/developer/installation).
See docs about [email worker](https://wr.do/docs/developer/cloudflare-email-worker).
## Local development
## Self-hosted
copy `.env.example` to `.env` and fill in the necessary environment variables.
### Deploy with Vercel
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/oiov/wr.do.git&project-name=wrdo&env=DATABASE_URL&env=AUTH_SECRET&env=RESEND_API_KEY&env=NEXT_PUBLIC_EMAIL_R2_DOMAIN&env=NEXT_PUBLIC_OPEN_SIGNUP&env=GITHUB_TOKEN)
Remember to fill in the necessary environment variables.
### Deploy with Docker Compose
Create a new folder and copy the `docker-compose.yml``.env` file to the folder.
```yml
- wrdo
| - docker-compose.yml
| - .env
```
Fill in the environment variables in the `.env` file, then:
```bash
docker compose up -d
```
## Local development
```bash
git clone https://github.com/oiov/wr.do
cd wr.do
pnpm install
```
copy `.env.example` to `.env` and fill in the necessary environment variables.
```bash
# run on localhost:3000
pnpm dev
```
@@ -68,21 +98,12 @@ pnpm db:push
Follow https://localhost:3000/setup
## Legitimacy review
- To avoid abuse, applications without website content will be rejected
- To avoid domain name conflicts, please check before applying
- Completed website construction or released open source project (ready to build website for open source project)
- Political sensitivity, violence, pornography, link jumping, VPN, reverse proxy services, and other illegal or sensitive content must not appear on the website
**Administrators will conduct domain name checks periodically to clean up domain names that violate the above rules, have no content, and are not open source related**
## Community Group
- Discord: https://discord.gg/AHPQYuZu3m
- 微信群:
![](https://wr.do/s/group)
<img width="300" src="https://wr.do/s/group" />
## License

View File

@@ -25,7 +25,7 @@ export default function LoginPage() {
>
<>
<Icons.chevronLeft className="mr-2 size-4" />
Back
Back {siteConfig.openSignup ? "Home" : ""}
</>
</Link>
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">

View File

@@ -140,12 +140,12 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
<CardHeader className="flex flex-row items-center">
{action.includes("/admin") ? (
<CardDescription className="text-balance text-lg font-bold">
<span>Total Records:</span>{" "}
<span>Total Subdomains:</span>{" "}
<span className="font-bold">{data && data.total}</span>
</CardDescription>
) : (
<div className="grid gap-2">
<CardTitle>DNS Records</CardTitle>
<CardTitle>Subdomains</CardTitle>
<CardDescription className="hidden text-balance sm:block">
Please read the{" "}
<Link

View File

@@ -4,6 +4,8 @@ import { FeatureMap, getDomainsByFeatureClient } from "@/lib/dto/domains";
import { checkUserStatus } from "@/lib/dto/user";
import { getCurrentUser } from "@/lib/session";
export const dynamic = "force-dynamic";
// Get domains by feature for frontend
export async function GET(req: NextRequest) {
try {

View File

@@ -1,14 +1,11 @@
import { ipAddress } from "@vercel/functions";
import { getIpInfo } from "@/lib/utils";
import { extractRealIP, getIpInfo } from "@/lib/geo";
export async function GET(req: Request) {
try {
const data = getIpInfo(req);
const ip = ipAddress(req);
const data = await getIpInfo(req);
return Response.json({
ip,
ip: data.ip,
city: data.city,
region: data.region,
country: data.country,

View File

@@ -3,7 +3,8 @@ import TurndownService from "turndown";
import { checkApiKey } from "@/lib/dto/api-key";
import { createScrapeMeta } from "@/lib/dto/scrape";
import { getIpInfo, isLink } from "@/lib/utils";
import { getIpInfo } from "@/lib/geo";
import { isLink } from "@/lib/utils";
export const revalidate = 600;
export const dynamic = "force-dynamic";
@@ -70,9 +71,9 @@ export async function GET(req: Request) {
const markdown = turndownService.turndown(mainContent || "");
const stats = getIpInfo(req);
const stats = await getIpInfo(req);
await createScrapeMeta({
ip: stats.ip,
ip: stats.ip || "::1",
type: "markdown",
referer: stats.referer,
city: stats.city,

View File

@@ -2,7 +2,8 @@ import cheerio from "cheerio";
import { checkApiKey } from "@/lib/dto/api-key";
import { createScrapeMeta } from "@/lib/dto/scrape";
import { getIpInfo, isLink, removeUrlSuffix } from "@/lib/utils";
import { getIpInfo } from "@/lib/geo";
import { isLink, removeUrlSuffix } from "@/lib/utils";
export const revalidate = 600;
export const dynamic = "force-dynamic";
@@ -84,9 +85,9 @@ export async function GET(req: Request) {
$("meta[name='author']").attr("content") ||
$("meta[property='author']").attr("content");
const stats = getIpInfo(req);
const stats = await getIpInfo(req);
await createScrapeMeta({
ip: stats.ip,
ip: stats.ip || "::1",
type: "meta-info",
referer: stats.referer,
city: stats.city,

View File

@@ -2,9 +2,10 @@ import { ImageResponse } from "@vercel/og";
import { checkApiKey } from "@/lib/dto/api-key";
import { createScrapeMeta } from "@/lib/dto/scrape";
import { getIpInfo } from "@/lib/geo";
import { WRDO_QR_LOGO } from "@/lib/qr/constants";
import { QRCodeSVG } from "@/lib/qr/utils";
import { getIpInfo, getSearchParams } from "@/lib/utils";
import { getSearchParams } from "@/lib/utils";
import { getQRCodeQuerySchema } from "@/lib/validations/qr";
const CORS_HEADERS = {
@@ -41,9 +42,9 @@ export async function GET(req: Request) {
);
}
const stats = getIpInfo(req);
const stats = await getIpInfo(req);
await createScrapeMeta({
ip: stats.ip,
ip: stats.ip || "::1",
type: "qrcode",
referer: stats.referer,
city: stats.city,

View File

@@ -1,7 +1,8 @@
import { env } from "@/env.mjs";
import { checkApiKey } from "@/lib/dto/api-key";
import { createScrapeMeta } from "@/lib/dto/scrape";
import { getIpInfo, isLink } from "@/lib/utils";
import { getIpInfo } from "@/lib/geo";
import { isLink } from "@/lib/utils";
export const revalidate = 60;
@@ -69,9 +70,9 @@ export async function GET(req: Request) {
);
}
const stats = getIpInfo(req);
const stats = await getIpInfo(req);
await createScrapeMeta({
ip: stats.ip,
ip: stats.ip || "::1",
type: "screenshot",
referer: stats.referer,
city: stats.city,

View File

@@ -2,7 +2,8 @@ import cheerio from "cheerio";
import { checkApiKey } from "@/lib/dto/api-key";
import { createScrapeMeta } from "@/lib/dto/scrape";
import { getIpInfo, isLink } from "@/lib/utils";
import { getIpInfo } from "@/lib/geo";
import { isLink } from "@/lib/utils";
export const revalidate = 600;
export const dynamic = "force-dynamic";
@@ -63,9 +64,9 @@ export async function GET(req: Request) {
$("style").remove();
const text = $("body").text().trim();
const stats = getIpInfo(req);
const stats = await getIpInfo(req);
await createScrapeMeta({
ip: stats.ip,
ip: stats.ip || "::1",
type: "text",
referer: stats.referer,
city: stats.city,

View File

@@ -22,6 +22,7 @@ export const {
handlers: { GET, POST },
auth,
} = NextAuth({
trustHost: true, // TODO: Test with docker
adapter: PrismaAdapter(prisma),
session: { strategy: "jwt" },
pages: {

View File

@@ -10,27 +10,19 @@ interface GitHubResponse {
}
async function getGitHubStars(owner: string, repo: string) {
const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
throw new Error("GitHub token is not configured");
}
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
headers: {
Accept: "application/vnd.github.v3+json",
Authorization: `token ${githubToken}`,
"User-Agent": "NextJS-App",
const res = await fetch(
`https://wr.do/api/github?owner=${owner}&repo=${repo}`,
{
next: { revalidate: 3600 },
},
next: { revalidate: 3600 },
});
);
if (!res.ok) {
throw new Error("Failed to fetch GitHub stars");
}
const data: GitHubResponse = await res.json();
return data.stargazers_count;
const data = await res.json();
return data.stars;
}
interface Props {

View File

@@ -1,7 +1,7 @@
import { SidebarNavItem, SiteConfig } from "types";
import { env } from "@/env.mjs";
const site_url = env.NEXT_PUBLIC_APP_URL;
const site_url = env.NEXT_PUBLIC_APP_URL || "http://localhost:3030";
const open_signup = env.NEXT_PUBLIC_OPEN_SIGNUP;
const email_r2_domain = env.NEXT_PUBLIC_EMAIL_R2_DOMAIN || "";

View File

@@ -0,0 +1,48 @@
---
title: Deploy Guide
description: Choose your deployment method
---
## Deploy with Vercel (Recommended)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/oiov/wr.do.git&project-name=wrdo&env=DATABASE_URL&env=AUTH_SECRET&env=RESEND_API_KEY&env=NEXT_PUBLIC_EMAIL_R2_DOMAIN&env=NEXT_PUBLIC_OPEN_SIGNUP&env=GITHUB_TOKEN)
Remember to fill in the necessary environment variables.
## Deploy with Docker Compose
<Callout type="warning" twClass="mt-4">
Please create your database instance before deployment.
Set `SKIP_DB_CHECK` and `SKIP_DB_MIGRATION` to `false` in the `.env` file, this will start the database check and migration.
</Callout>
Create a new folder and copy the `docker-compose.yml`、`.env` file to the folder.
```bash
- wrdo
| - docker-compose.yml
| - .env
```
Fill in the environment variables in the `.env` file, then:
```bash
docker compose up -d
```
## Deploy with Docker Compose (Local DB)
Create a new folder and copy the `docker-compose-localdb.yml`、`.env` file to the folder.
```bash
- wrdo
| - docker-compose.yml
| - .env
```
Fill in the environment variables in the `.env` file, then:
```bash
docker compose up -d
```

View File

@@ -205,6 +205,10 @@ Follow the steps below:
<strong>You must add at least one domain to start using short links, email or subdomain management features.</strong>
</Callout>
## 9. Deploy
See [Deploy Guide](/docs/developer/deploy).
## Q & A
### Worker Error - Too many redirects

View File

@@ -15,13 +15,20 @@ description: Welcome to the WR.DO documentation.
## Screenshots
<img className="rounded-xl border mt-4 md:rounded-lg shadow-md" src="/_static/images/light-preview.png"/>
<img className="rounded-xl border mt-4 md:rounded-lg shadow-md" src="/_static/images/example_02.png"/>
<img className="rounded-xl border mt-4 md:rounded-lg shadow-md" src="/_static/images/example_01.png"/>
<img className="rounded-xl border mt-4 md:rounded-lg shadow-md" src="/_static/images/example_03.png"/>
<table>
<tr>
<td><img src="https://wr.do/_static/images/light-preview.png" /></td>
<td><img src="https://wr.do/_static/images/example_02.png" /></td>
</tr>
<tr>
<td><img src="https://wr.do/_static/images/example_01.png" /></td>
<td><img src="https://wr.do/_static/images/realtime-globe.png" /></td>
</tr>
<tr>
<td><img src="https://wr.do/_static/images/example_03.png" /></td>
<td><img src="https://wr.do/_static/images/domains.png" /></td>
</tr>
</table>
## Quick Start

View File

@@ -0,0 +1,53 @@
services:
app:
image: ghcr.io/oiov/wr.do/wrdo:${TAG:-latest}
container_name: wrdo
ports:
- "3000:3000"
environment:
NODE_ENV: production
DATABASE_URL: postgres://postgres:postgres@postgres:5432/wrdo
AUTH_SECRET: ${AUTH_SECRET:-your-auth-secret}
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
NEXT_PUBLIC_OPEN_SIGNUP: ${NEXT_PUBLIC_OPEN_SIGNUP}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
GITHUB_ID: ${GITHUB_ID}
GITHUB_SECRET: ${GITHUB_SECRET}
LinuxDo_CLIENT_ID: ${LinuxDo_CLIENT_ID}
LinuxDo_CLIENT_SECRET: ${LinuxDo_CLIENT_SECRET}
RESEND_API_KEY: ${RESEND_API_KEY}
NEXT_PUBLIC_EMAIL_R2_DOMAIN: ${NEXT_PUBLIC_EMAIL_R2_DOMAIN}
NEXT_PUBLIC_GOOGLE_ID: ${NEXT_PUBLIC_GOOGLE_ID}
SCREENSHOTONE_BASE_URL: ${SCREENSHOTONE_BASE_URL}
GITHUB_TOKEN: ${GITHUB_TOKEN}
SKIP_DB_CHECK: ${SKIP_DB_CHECK}
SKIP_DB_MIGRATION: ${SKIP_DB_MIGRATION}
depends_on:
- postgres
networks:
- wrdo-network
restart: unless-stopped
postgres:
image: postgres:16-alpine
container_name: postgres
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=wrdo
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- wrdo-network
restart: unless-stopped
volumes:
postgres-data:
name: wrdo-postgres-data
networks:
wrdo-network:
driver: bridge

33
docker-compose.yml Normal file
View File

@@ -0,0 +1,33 @@
services:
app:
image: ghcr.io/oiov/wr.do/wrdo:${TAG:-latest}
container_name: wrdo
ports:
- "3000:3000"
environment:
NODE_ENV: production
DATABASE_URL: ${DATABASE_URL}
AUTH_SECRET: ${AUTH_SECRET:-your-auth-secret}
AUTH_URL: ${AUTH_URL}
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
NEXT_PUBLIC_OPEN_SIGNUP: ${NEXT_PUBLIC_OPEN_SIGNUP}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
GITHUB_ID: ${GITHUB_ID}
GITHUB_SECRET: ${GITHUB_SECRET}
LinuxDo_CLIENT_ID: ${LinuxDo_CLIENT_ID}
LinuxDo_CLIENT_SECRET: ${LinuxDo_CLIENT_SECRET}
RESEND_API_KEY: ${RESEND_API_KEY}
NEXT_PUBLIC_EMAIL_R2_DOMAIN: ${NEXT_PUBLIC_EMAIL_R2_DOMAIN}
NEXT_PUBLIC_GOOGLE_ID: ${NEXT_PUBLIC_GOOGLE_ID}
SCREENSHOTONE_BASE_URL: ${SCREENSHOTONE_BASE_URL}
GITHUB_TOKEN: ${GITHUB_TOKEN}
SKIP_DB_CHECK: ${SKIP_DB_CHECK}
SKIP_DB_MIGRATION: ${SKIP_DB_MIGRATION}
networks:
- wrdo-network
restart: unless-stopped
networks:
wrdo-network:
driver: bridge

View File

@@ -6,22 +6,22 @@ export const env = createEnv({
// This is optional because it's only used in development.
// See https://next-auth.js.org/deployment.
NEXTAUTH_URL: z.string().url().optional(),
AUTH_SECRET: z.string().min(1),
AUTH_SECRET: z.string().optional(),
GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),
GITHUB_ID: z.string().optional(),
GITHUB_SECRET: z.string().optional(),
LinuxDo_CLIENT_ID: z.string().optional(),
LinuxDo_CLIENT_SECRET: z.string().optional(),
DATABASE_URL: z.string().min(1),
DATABASE_URL: z.string().optional(),
RESEND_API_KEY: z.string().optional(),
SCREENSHOTONE_BASE_URL: z.string().optional(),
GITHUB_TOKEN: z.string().optional(),
},
client: {
NEXT_PUBLIC_APP_URL: z.string().min(1),
NEXT_PUBLIC_APP_URL: z.string().optional(),
NEXT_PUBLIC_OPEN_SIGNUP: z.string().min(1).default("1"),
NEXT_PUBLIC_EMAIL_R2_DOMAIN: z.string().min(1),
NEXT_PUBLIC_EMAIL_R2_DOMAIN: z.string().optional(),
},
runtimeEnv: {
NEXTAUTH_URL: process.env.NEXTAUTH_URL,

View File

@@ -2,7 +2,7 @@ import { Resend } from "resend";
import { env } from "@/env.mjs";
export const resend = new Resend(env.RESEND_API_KEY);
export const resend = new Resend(env.RESEND_API_KEY || "re_key");
export function getVerificationEmailHtml({
url,

150
lib/geo.ts Normal file
View File

@@ -0,0 +1,150 @@
import { userAgent } from "next/server";
import { Geo, geolocation, ipAddress } from "@vercel/functions";
import { NextAuthRequest } from "next-auth/lib";
import UAParser from "ua-parser-js";
interface GeoLocation extends Geo {
ip?: string;
}
const isVercel = process.env.VERCEL;
export async function getGeolocation(
req: NextAuthRequest,
ip: string,
): Promise<GeoLocation | null> {
// console.log("[Runtime Env]", isVercel ? "Vercel" : "Other");
if (isVercel) {
return geolocation(req);
} else {
return await getClientGeolocationWithIpApi(ip);
}
}
export function getUserAgent(req: NextAuthRequest) {
if (isVercel) {
return userAgent(req);
} else {
const headers = req.headers;
const userAgent = headers.get("user-agent") || "";
const parser = new UAParser(userAgent);
return {
browser: parser.getBrowser(),
device: parser.getDevice(),
os: parser.getOS(),
engine: parser.getEngine(),
cpu: parser.getCPU(),
isBot: false,
};
}
}
export async function getClientGeolocation(
req,
ip,
): Promise<GeoLocation | null> {
// const new_headers = new Headers();
// new_headers.set("X-Forwarded-For", ip);
// new_headers.set("User-Agent", req.headers.get("user-agent") || "");
const response = await fetch(`https://ip.wr.do/api?ip=${ip}`);
if (!response.ok) return null;
return await response.json();
}
export async function getClientGeolocationWithIpApi(ip: string) {
const response = await fetch(`http://ip-api.com/json/${ip}`);
if (!response.ok) return null;
const res = await response.json();
// {
// "query": "154.64.226.29",
// "status": "success",
// "continent": "North America",
// "continentCode": "NA",
// "country": "United States",
// "countryCode": "US",
// "region": "CA",
// "regionName": "California",
// "city": "Los Angeles",
// "district": "",
// "zip": "90009",
// "lat": 34.0549,
// "lon": -118.243,
// "timezone": "America/Los_Angeles",
// "offset": -25200,
// "currency": "USD",
// "isp": "Cogent Communications",
// "org": "NetLab Global",
// "as": "AS51847 Nearoute Limited",
// "asname": "NEAROUTE",
// "mobile": true,
// "proxy": true,
// "hosting": false
// }
return {
ip: res.query,
country: res.countryCode,
countryName: res.country,
region: res.region,
regionName: res.regionName,
city: res.city,
latitude: res.lat.toString(),
longitude: res.lon.toString(),
timezone: res.timezone,
};
}
export function extractRealIP(headers: Headers): string {
const ipHeaders = [
"X-Forwarded-For",
"X-Real-IP",
"CF-Connecting-IP",
"X-Client-IP",
"X-Cluster-Client-IP",
];
for (const header of ipHeaders) {
const value = headers.get(header);
if (value) {
const ip = value.split(",")[0].trim();
if (isValidIP(ip)) {
return ip;
}
}
}
return "::1";
}
function isValidIP(ip: string): boolean {
// IPv4 正则
const ipv4Regex =
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// IPv6 正则(简化版)
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}
export async function getIpInfo(req) {
const headers = req.headers;
const ip = isVercel ? ipAddress(req) : extractRealIP(headers);
const ua = getUserAgent(req);
const geo = await getGeolocation(req, ip || "::1");
const userLanguage =
req.headers.get("accept-language")?.split(",")[0] || "en-US";
return {
referer: headers.get("referer") || "(None)",
ip: isVercel ? ip : geo?.ip,
city: geo?.city || "",
region: geo?.region || "",
country: geo?.country || "",
latitude: geo?.latitude || "",
longitude: geo?.longitude || "",
flag: geo?.flag,
lang: userLanguage,
device: ua.device.model || "Unknown",
browser: ua.browser.name || "Unknown",
};
}

View File

@@ -1,12 +1,9 @@
import crypto from "crypto";
import { Metadata } from "next";
import { geolocation } from "@vercel/functions";
import { clsx, type ClassValue } from "clsx";
import ms from "ms";
import { twMerge } from "tailwind-merge";
import UAParser from "ua-parser-js";
import { env } from "@/env.mjs";
import { siteConfig } from "@/config/site";
import { TIME_RANGES } from "./enums";
@@ -94,16 +91,12 @@ export function formatTime(input: string | number): string {
const locale = navigator.language || "en-US";
return date.toLocaleTimeString(locale, {
second: "numeric",
// second: "numeric",
minute: "numeric",
hour: "numeric",
});
}
export function absoluteUrl(path: string) {
return `${env.NEXT_PUBLIC_APP_URL}${path}`;
}
// Utils from precedent.dev
export const timeAgo = (timestamp: Date, timeOnly?: boolean): string => {
if (!timestamp) return "never";
@@ -252,33 +245,6 @@ export function removeUrlSuffix(url: string): string {
return url.startsWith("http") ? url.split("//")[1] : url;
}
export function getIpInfo(req: Request) {
const geo = geolocation(req);
const ua = req.headers.get("user-agent") || "";
const parser = new UAParser();
parser.setUA(ua);
const browser = parser.getBrowser();
const device = parser.getDevice();
const referer = req.headers.get("referer") || "(None)";
const ip = req.headers.get("X-Forwarded-For") || "127.0.0.1";
const userLanguage =
req.headers.get("accept-language")?.split(",")[0] || "en-US";
return {
referer,
ip,
city: geo?.city || "",
region: geo?.region || "",
country: geo?.country || "",
latitude: geo?.latitude || "",
longitude: geo?.longitude || "",
flag: geo?.flag,
lang: userLanguage,
device: device.model || "Unknown",
browser: browser.name || "Unknown",
};
}
export function toCamelCase(str: string) {
return str
.split("-")

View File

@@ -1,15 +1,17 @@
import { NextResponse } from "next/server";
import { geolocation } from "@vercel/functions";
import { ipAddress } from "@vercel/functions";
import { auth } from "auth";
import { NextAuthRequest } from "next-auth/lib";
import UAParser from "ua-parser-js";
import { siteConfig } from "./config/site";
import { extractRealIP, getGeolocation, getUserAgent } from "./lib/geo";
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
const isVercel = process.env.VERCEL;
const redirectMap = {
"Missing[0000]": "/docs/short-urls#missing-links",
"Expired[0001]": "/docs/short-urls#expired-links",
@@ -19,7 +21,6 @@ const redirectMap = {
"IncorrectPassword[0005]": "/password-prompt?error=1&slug=",
};
// 提取短链接处理逻辑
async function handleShortUrl(req: NextAuthRequest) {
if (!req.url.includes("/s/")) return NextResponse.next();
@@ -27,9 +28,11 @@ async function handleShortUrl(req: NextAuthRequest) {
if (!slug)
return NextResponse.redirect(`${siteConfig.url}/docs/short-urls`, 302);
const geo = geolocation(req);
const headers = req.headers;
const { browser, device } = parseUserAgent(headers.get("user-agent") || "");
const ip = isVercel ? ipAddress(req) : extractRealIP(headers);
const ua = getUserAgent(req);
const geo = await getGeolocation(req, ip || "::1");
const url = new URL(req.url);
const password = url.searchParams.get("password") || "";
@@ -37,19 +40,25 @@ async function handleShortUrl(req: NextAuthRequest) {
const trackingData = {
slug,
referer: headers.get("referer") || "(None)",
ip: headers.get("X-Forwarded-For"),
ip,
city: geo?.city,
region: geo?.region,
country: geo?.country,
latitude: geo?.latitude,
longitude: geo?.longitude,
flag: geo?.flag,
lang: headers.get("accept-language")?.split(",")[0],
device: device.model || "Unknown",
browser: browser.name || "Unknown",
lang: headers.get("accept-language")?.split(",")[0] || "Unknown",
device: ua.device.model || "Unknown",
browser: ua.browser.name || "Unknown",
engine: ua.engine.name || "",
os: ua.os.name || "",
cpu: ua.cpu.architecture || "",
isBot: ua.isBot,
password,
};
// console.log("Tracking data:", trackingData, siteConfig.url);
const res = await fetch(`${siteConfig.url}/api/s`, {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -90,22 +99,11 @@ async function handleShortUrl(req: NextAuthRequest) {
return NextResponse.redirect(target, 302);
}
// 提取 slug
function extractSlug(url: string): string | null {
const match = url.match(/([^/?]+)(?:\?.*)?$/);
return match ? match[1] : null;
}
// 解析用户代理
const parser = new UAParser();
function parseUserAgent(ua: string) {
parser.setUA(ua);
return {
browser: parser.getBrowser(),
device: parser.getDevice(),
};
}
export default auth(async (req) => {
try {
return await handleShortUrl(req);

View File

@@ -6,6 +6,7 @@ import("./env.mjs");
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
output: "standalone",
images: {
remotePatterns: [
{

View File

@@ -11,12 +11,18 @@
"postbuild": "next-sitemap",
"turbo": "next dev --turbo",
"start": "next start",
"start-docker": "npm-run-all check-db start-server",
"start-server": "node server.js",
"lint": "next lint",
"preview": "next build && next start",
"postinstall": "prisma generate",
"db:push": "npx prisma migrate deploy",
"email": "email dev --dir emails --port 3333",
"remove-content": "node ./setup.mjs"
"remove-content": "node ./setup.mjs",
"check-db": "node scripts/check-db.js"
},
"prisma": {
"schema": "./prisma/schema.prisma"
},
"dependencies": {
"@auth/prisma-adapter": "^2.4.1",
@@ -68,6 +74,7 @@
"@vercel/functions": "^1.4.0",
"@vercel/og": "^0.6.2",
"cheerio": "1.0.0-rc.12",
"chalk": "^4.1.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -77,6 +84,7 @@
"d3-scale": "^4.0.2",
"d3-scale-chromatic": "^3.1.0",
"date-fns": "^3.6.0",
"dotenv": "^10.0.0",
"framer-motion": "^12.5.0",
"globe.gl": "^2.41.4",
"lodash": "^4.17.21",
@@ -93,6 +101,7 @@
"next-themes": "^0.3.0",
"next-view-transitions": "^0.3.0",
"nodemailer": "^6.9.14",
"npm-run-all": "^4.1.5",
"peerjs": "^1.5.4",
"prop-types": "^15.8.1",
"react": "18.3.1",
@@ -112,6 +121,7 @@
"shiki": "^1.11.0",
"sonner": "^1.5.0",
"swr": "^2.2.5",
"semver": "^7.5.4",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"three": "^0.176.0",

232
pnpm-lock.yaml generated
View File

@@ -152,6 +152,9 @@ importers:
'@vercel/og':
specifier: ^0.6.2
version: 0.6.2
chalk:
specifier: ^4.1.1
version: 4.1.2
cheerio:
specifier: 1.0.0-rc.12
version: 1.0.0-rc.12
@@ -182,6 +185,9 @@ importers:
date-fns:
specifier: ^3.6.0
version: 3.6.0
dotenv:
specifier: ^10.0.0
version: 10.0.0
framer-motion:
specifier: ^12.5.0
version: 12.5.0(@emotion/is-prop-valid@0.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -230,6 +236,9 @@ importers:
nodemailer:
specifier: ^6.9.14
version: 6.9.14
npm-run-all:
specifier: ^4.1.5
version: 4.1.5
peerjs:
specifier: ^1.5.4
version: 1.5.4
@@ -275,6 +284,9 @@ importers:
resend:
specifier: ^3.4.0
version: 3.4.0
semver:
specifier: ^7.5.4
version: 7.6.3
sharp:
specifier: ^0.33.4
version: 0.33.4
@@ -4053,6 +4065,10 @@ packages:
countup.js@2.8.0:
resolution: {integrity: sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ==}
cross-spawn@6.0.6:
resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==}
engines: {node: '>=4.8'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -4411,6 +4427,10 @@ packages:
resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
engines: {node: '>=8'}
dotenv@10.0.0:
resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==}
engines: {node: '>=10'}
dotenv@16.0.3:
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
engines: {node: '>=12'}
@@ -5114,6 +5134,9 @@ packages:
resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==}
engines: {node: '>=6'}
hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
html-to-text@9.0.5:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'}
@@ -5465,6 +5488,9 @@ packages:
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
json-parse-better-errors@1.0.2:
resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==}
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
@@ -5549,6 +5575,10 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
load-json-file@4.0.0:
resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
engines: {node: '>=4'}
loader-runner@4.3.0:
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
engines: {node: '>=6.11.5'}
@@ -5635,10 +5665,6 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
lucide-react@0.414.0:
resolution: {integrity: sha512-Krr/MHg9AWoJc52qx8hyJ64X9++JNfS1wjaJviLM1EP/68VNB7Tv0VMldLCB1aUe6Ka9QxURPhQm/eB6cqOM3A==}
peerDependencies:
@@ -5742,6 +5768,10 @@ packages:
resolution: {integrity: sha512-f16coDZlTG1jskq3mxarwB+fGRrd0uXWt+o1WIhRfOwbXQZqUDsTVxQBFK9JjRQHblg8eAG2JSbprDXKjc7ijQ==}
engines: {node: '>= 4.0.0'}
memorystream@0.3.1:
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
engines: {node: '>= 0.10.0'}
meow@12.1.1:
resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==}
engines: {node: '>=16.10'}
@@ -6034,6 +6064,9 @@ packages:
sass:
optional: true
nice-try@1.0.5:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
@@ -6052,6 +6085,9 @@ packages:
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
hasBin: true
normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@@ -6060,6 +6096,11 @@ packages:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'}
npm-run-all@4.1.5:
resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==}
engines: {node: '>= 4'}
hasBin: true
npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
@@ -6186,6 +6227,10 @@ packages:
parse-entities@4.0.1:
resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==}
parse-json@4.0.0:
resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
engines: {node: '>=4'}
parse-json@5.2.0:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
@@ -6223,6 +6268,10 @@ packages:
path-is-inside@1.0.2:
resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==}
path-key@2.0.1:
resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
engines: {node: '>=4'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
@@ -6238,6 +6287,10 @@ packages:
resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
engines: {node: '>=16 || 14 >=14.17'}
path-type@3.0.0:
resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
engines: {node: '>=4'}
path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
@@ -6277,10 +6330,19 @@ packages:
resolution: {integrity: sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==}
engines: {node: '>=10'}
pidtree@0.3.1:
resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==}
engines: {node: '>=0.10'}
hasBin: true
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
pify@3.0.0:
resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
engines: {node: '>=4'}
pify@4.0.1:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'}
@@ -6638,6 +6700,10 @@ packages:
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
read-pkg@3.0.0:
resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
engines: {node: '>=4'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@@ -6854,13 +6920,12 @@ packages:
selderee@0.11.0:
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true
semver@7.6.0:
resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
engines: {node: '>=10'}
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
semver@7.6.3:
@@ -6892,10 +6957,18 @@ packages:
resolution: {integrity: sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==}
engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@1.2.0:
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
engines: {node: '>=0.10.0'}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@1.0.0:
resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
engines: {node: '>=0.10.0'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
@@ -7004,6 +7077,18 @@ packages:
spawn-command@0.0.2:
resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==}
spdx-correct@3.2.0:
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
spdx-exceptions@2.5.0:
resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==}
spdx-expression-parse@3.0.1:
resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
spdx-license-ids@3.0.21:
resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
@@ -7034,6 +7119,10 @@ packages:
resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==}
engines: {node: '>= 0.4'}
string.prototype.padend@3.1.6:
resolution: {integrity: sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==}
engines: {node: '>= 0.4'}
string.prototype.repeat@1.0.0:
resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==}
@@ -7521,6 +7610,9 @@ packages:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -7707,9 +7799,6 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
@@ -9016,7 +9105,7 @@ snapshots:
'@babel/traverse': 7.24.6
'@babel/types': 7.24.6
prettier: 3.3.3
semver: 7.6.0
semver: 7.6.3
transitivePeerDependencies:
- supports-color
@@ -11989,6 +12078,14 @@ snapshots:
countup.js@2.8.0: {}
cross-spawn@6.0.6:
dependencies:
nice-try: 1.0.5
path-key: 2.0.1
semver: 5.7.2
shebang-command: 1.2.0
which: 1.3.1
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -12370,6 +12467,8 @@ snapshots:
dependencies:
is-obj: 2.0.0
dotenv@10.0.0: {}
dotenv@16.0.3: {}
dunder-proto@1.0.1:
@@ -13361,6 +13460,8 @@ snapshots:
hex-rgb@4.3.0: {}
hosted-git-info@2.8.9: {}
html-to-text@9.0.5:
dependencies:
'@selderee/plugin-htmlparser2': 0.11.0
@@ -13665,6 +13766,8 @@ snapshots:
json-buffer@3.0.1: {}
json-parse-better-errors@1.0.2: {}
json-parse-even-better-errors@2.3.1: {}
json-schema-traverse@0.4.1: {}
@@ -13736,6 +13839,13 @@ snapshots:
lines-and-columns@1.2.4: {}
load-json-file@4.0.0:
dependencies:
graceful-fs: 4.2.11
parse-json: 4.0.0
pify: 3.0.0
strip-bom: 3.0.0
loader-runner@4.3.0: {}
loader-utils@2.0.4:
@@ -13809,10 +13919,6 @@ snapshots:
dependencies:
yallist: 3.1.1
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
lucide-react@0.414.0(react@18.3.1):
dependencies:
react: 18.3.1
@@ -14072,6 +14178,8 @@ snapshots:
sonic-forest: 1.0.2(tslib@2.8.1)
tslib: 2.8.1
memorystream@0.3.1: {}
meow@12.1.1: {}
merge-stream@2.0.0: {}
@@ -14531,6 +14639,8 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
nice-try@1.0.5: {}
no-case@3.0.4:
dependencies:
lower-case: 2.0.2
@@ -14546,10 +14656,29 @@ snapshots:
dependencies:
abbrev: 2.0.0
normalize-package-data@2.5.0:
dependencies:
hosted-git-info: 2.8.9
resolve: 1.22.8
semver: 5.7.2
validate-npm-package-license: 3.0.4
normalize-path@3.0.0: {}
normalize-range@0.1.2: {}
npm-run-all@4.1.5:
dependencies:
ansi-styles: 3.2.1
chalk: 2.4.2
cross-spawn: 6.0.6
memorystream: 0.3.1
minimatch: 3.1.2
pidtree: 0.3.1
read-pkg: 3.0.0
shell-quote: 1.8.1
string.prototype.padend: 3.1.6
npm-run-path@4.0.1:
dependencies:
path-key: 3.1.1
@@ -14699,6 +14828,11 @@ snapshots:
is-decimal: 2.0.1
is-hexadecimal: 2.0.1
parse-json@4.0.0:
dependencies:
error-ex: 1.3.2
json-parse-better-errors: 1.0.2
parse-json@5.2.0:
dependencies:
'@babel/code-frame': 7.24.6
@@ -14737,6 +14871,8 @@ snapshots:
path-is-inside@1.0.2: {}
path-key@2.0.1: {}
path-key@3.1.1: {}
path-key@4.0.0: {}
@@ -14748,6 +14884,10 @@ snapshots:
lru-cache: 10.0.2
minipass: 7.0.4
path-type@3.0.0:
dependencies:
pify: 3.0.0
path-type@4.0.0: {}
pbf@3.3.0:
@@ -14782,8 +14922,12 @@ snapshots:
picomatch@3.0.1: {}
pidtree@0.3.1: {}
pify@2.3.0: {}
pify@3.0.0: {}
pify@4.0.1: {}
pinkie-promise@2.0.1:
@@ -15179,6 +15323,12 @@ snapshots:
dependencies:
pify: 2.3.0
read-pkg@3.0.0:
dependencies:
load-json-file: 4.0.0
normalize-package-data: 2.5.0
path-type: 3.0.0
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
@@ -15493,11 +15643,9 @@ snapshots:
dependencies:
parseley: 0.12.1
semver@6.3.1: {}
semver@5.7.2: {}
semver@7.6.0:
dependencies:
lru-cache: 6.0.0
semver@6.3.1: {}
semver@7.6.3: {}
@@ -15533,7 +15681,7 @@ snapshots:
dependencies:
color: 4.2.3
detect-libc: 2.0.3
semver: 7.6.0
semver: 7.6.3
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.4
'@img/sharp-darwin-x64': 0.33.4
@@ -15555,10 +15703,16 @@ snapshots:
'@img/sharp-win32-ia32': 0.33.4
'@img/sharp-win32-x64': 0.33.4
shebang-command@1.2.0:
dependencies:
shebang-regex: 1.0.0
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@1.0.0: {}
shebang-regex@3.0.0: {}
shell-quote@1.8.1: {}
@@ -15672,6 +15826,20 @@ snapshots:
spawn-command@0.0.2: {}
spdx-correct@3.2.0:
dependencies:
spdx-expression-parse: 3.0.1
spdx-license-ids: 3.0.21
spdx-exceptions@2.5.0: {}
spdx-expression-parse@3.0.1:
dependencies:
spdx-exceptions: 2.5.0
spdx-license-ids: 3.0.21
spdx-license-ids@3.0.21: {}
split2@4.2.0: {}
sprintf-js@1.0.3: {}
@@ -15711,6 +15879,13 @@ snapshots:
set-function-name: 2.0.2
side-channel: 1.0.6
string.prototype.padend@3.1.6:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.23.3
es-object-atoms: 1.1.1
string.prototype.repeat@1.0.0:
dependencies:
define-properties: 1.2.1
@@ -15721,19 +15896,19 @@ snapshots:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.23.3
es-object-atoms: 1.0.0
es-object-atoms: 1.1.1
string.prototype.trimend@1.0.8:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-object-atoms: 1.0.0
es-object-atoms: 1.1.1
string.prototype.trimstart@1.0.8:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-object-atoms: 1.0.0
es-object-atoms: 1.1.1
string_decoder@1.3.0:
dependencies:
@@ -16257,6 +16432,11 @@ snapshots:
uuid@9.0.1: {}
validate-npm-package-license@3.0.4:
dependencies:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
vary@1.1.2: {}
vaul@0.9.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
@@ -16571,8 +16751,6 @@ snapshots:
yallist@3.1.1: {}
yallist@4.0.0: {}
yaml@1.10.2: {}
yaml@2.3.4: {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -1,39 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://wr.do/robots.txt</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/authentification</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/cloudflare</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/cloudflare-email-worker</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/components</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/config-files</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/database</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/email</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/installation</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/markdown-files</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/quick-start</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/dns-records</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/emails</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/cloudflare</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/other</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/vercel</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/zeabur</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/icon</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/markdown</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/meta-info</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/qrcode</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/screenshot</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/text</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/plan</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/quick-start</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/short-urls</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/wroom</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/pricing</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/privacy</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/terms</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/chat</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/manifest.json</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/password-prompt</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/opengraph-image.jpg</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/robots.txt</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/pricing</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/privacy</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/terms</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/authentification</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/cloudflare</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/cloudflare-email-worker</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/components</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/config-files</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/database</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/email</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/installation</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/markdown-files</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/quick-start</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/dns-records</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/emails</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/cloudflare</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/other</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/vercel</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/zeabur</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/icon</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/markdown</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/meta-info</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/qrcode</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/screenshot</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/text</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/plan</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/quick-start</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/short-urls</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/wroom</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/chat</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/password-prompt</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/manifest.json</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/opengraph-image.jpg</loc><lastmod>2025-05-26T09:40:46.922Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
</urlset>

File diff suppressed because one or more lines are too long

96
scripts/check-db.js Normal file
View File

@@ -0,0 +1,96 @@
/* eslint-disable no-console */
require("dotenv").config();
const { PrismaClient } = require("@prisma/client");
const chalk = require("chalk");
const { execSync } = require("child_process");
const semver = require("semver");
if (process.env.SKIP_DB_CHECK === "true") {
console.log("Skipping database check.");
process.exit(0);
}
function getDatabaseType(url = process.env.DATABASE_URL) {
const type = url && url.split(":")[0];
if (type === "postgres") {
return "postgresql";
}
return type;
}
const prisma = new PrismaClient();
function success(msg) {
console.log(chalk.greenBright(`${msg}`));
}
function error(msg) {
console.log(chalk.redBright(`${msg}`));
}
async function checkEnv() {
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is not defined.");
} else {
success("DATABASE_URL is defined.");
}
}
async function checkConnection() {
try {
await prisma.$connect();
success("Database connection successful.");
} catch (e) {
throw new Error("Unable to connect to the database: " + e.message);
}
}
async function checkDatabaseVersion() {
const query = await prisma.$queryRaw`select version() as version`;
const version = semver.valid(semver.coerce(query[0].version));
const databaseType = getDatabaseType();
const minVersion = databaseType === "postgresql" ? "9.4.0" : "5.7.0";
if (semver.lt(version, minVersion)) {
throw new Error(
`Database version is not compatible. Please upgrade ${databaseType} version to ${minVersion} or greater`,
);
}
success("Database version check successful.");
}
async function applyMigration() {
if (process.env.SKIP_DB_MIGRATION === "false") {
console.log(execSync("prisma generate").toString());
console.log(execSync("prisma migrate deploy").toString());
success("Database is up to date.");
}
}
(async () => {
let err = false;
for (let fn of [
checkEnv,
checkConnection,
checkDatabaseVersion,
applyMigration,
]) {
try {
await fn();
} catch (e) {
error(e.message);
err = true;
} finally {
await prisma.$disconnect();
if (err) {
process.exit(1);
}
}
}
})();