├── src ├── enums │ └── nav-type.enum.ts ├── native.ts ├── index.ts ├── entities │ └── navigation.entity.ts ├── services │ ├── cbapp.service.ts │ └── collection.service.ts ├── startup.ts └── middlewares │ └── navigator.middleware.ts ├── .gitignore ├── .vscode └── launch.json ├── .eslintrc.js ├── tsconfig.json ├── .github └── workflows │ └── publish.yml ├── package.json ├── LICENSE ├── cloudbaserc.json └── README.md /src/enums/nav-type.enum.ts: -------------------------------------------------------------------------------- 1 | export const enum NavTypeEnum { 2 | domain = 0, 3 | url = 1, 4 | message = 2, 5 | rootMsg = 3, 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /functions 2 | 3 | dist 4 | node_modules 5 | package-lock.json 6 | 7 | .env.local 8 | .env.*.local 9 | 10 | TCBSAM.yaml 11 | yarn-error.log -------------------------------------------------------------------------------- /src/native.ts: -------------------------------------------------------------------------------- 1 | import { NativeStartup } from "@halsp/native"; 2 | import startup from "./startup"; 3 | 4 | startup(new NativeStartup().useHttpJsonBody()).dynamicListen(); 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { LambdaStartup } from "@halsp/lambda"; 2 | import startup from "./startup"; 3 | 4 | const app = startup(new LambdaStartup()); 5 | export const main = (e: any, c: any) => app.run(e, c); 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "npm start", 6 | "name": "Ipare Http Debugger", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /src/entities/navigation.entity.ts: -------------------------------------------------------------------------------- 1 | import { NavTypeEnum } from "../enums/nav-type.enum"; 2 | 3 | export interface NavigationEntity { 4 | _id: string; 5 | to: string; 6 | code?: 301 | 302 | 303 | 307 | 308 | 400 | 401 | 403 | 404 | 500 | number; 7 | type: NavTypeEnum; 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | extends: [ 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:prettier/recommended", 6 | ], 7 | plugins: ["@typescript-eslint"], 8 | rules: { 9 | "@typescript-eslint/no-explicit-any": "off", 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/services/cbapp.service.ts: -------------------------------------------------------------------------------- 1 | import tcb = require("@cloudbase/node-sdk"); 2 | 3 | export class CbappService { 4 | public readonly app = tcb.init({ 5 | env: process.env.SCF_NAMESPACE ?? process.env.ENV_ID, 6 | secretId: process.env.SECRET_ID, 7 | secretKey: process.env.SECRET_KEY, 8 | }); 9 | public readonly db = this.app.database(); 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "outDir": "./dist/navigation", 6 | "target": "es2017", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "noImplicitAny": false, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "sourceMap": true 14 | }, 15 | "exclude": [ 16 | "dist", 17 | ] 18 | } -------------------------------------------------------------------------------- /src/services/collection.service.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "@cloudbase/node-sdk"; 2 | import { Inject } from "@halsp/inject"; 3 | import { CbappService } from "./cbapp.service"; 4 | 5 | export class CollectionService { 6 | @Inject 7 | private readonly cbappService!: CbappService; 8 | 9 | private getCollection(collection: string): Database.CollectionReference { 10 | return this.cbappService.db.collection(collection); 11 | } 12 | 13 | get navigation(): Database.CollectionReference { 14 | return this.getCollection("navigation"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [16.x] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: npm install 18 | - name: Setup ENV 19 | env: 20 | ENV: ${{ secrets.ENV }} 21 | run: | 22 | echo "$ENV" > ./.env.local 23 | - name: Deploy 24 | env: 25 | TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }} 26 | TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }} 27 | run: | 28 | npx tcb login --apiKeyId $TENCENT_SECRET_ID --apiKey $TENCENT_SECRET_KEY 29 | npx tcb framework deploy --mode local 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "navigation", 3 | "version": "0.3.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "halsp start --mode local", 7 | "build": "halsp build", 8 | "lint": "eslint . --ext .ts", 9 | "deploy": "tcb login && tcb framework deploy --mode local" 10 | }, 11 | "dependencies": { 12 | "@cloudbase/node-sdk": "^2.10.0", 13 | "@halsp/core": "^1.1.1", 14 | "@halsp/env": "^1.1.1", 15 | "@halsp/inject": "^1.1.1", 16 | "@halsp/lambda": "^1.1.1", 17 | "@halsp/logger": "^1.1.1" 18 | }, 19 | "devDependencies": { 20 | "@cloudbase/cli": "^2.2.8", 21 | "@halsp/cli": "^0.3.0", 22 | "@halsp/native": "^1.1.1", 23 | "@types/node": "^18.15.12", 24 | "@typescript-eslint/eslint-plugin": "^5.59.0", 25 | "@typescript-eslint/parser": "^5.59.0", 26 | "eslint": "^8.38.0", 27 | "eslint-config-prettier": "^8.8.0", 28 | "eslint-plugin-prettier": "^4.2.1", 29 | "prettier": "^2.8.7" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/startup.ts: -------------------------------------------------------------------------------- 1 | import "@halsp/inject"; 2 | import "@halsp/logger"; 3 | 4 | import { HttpStartup } from "@halsp/http"; 5 | import { getVersion } from "@halsp/env"; 6 | import { CollectionService } from "./services/collection.service"; 7 | import { CbappService } from "./services/cbapp.service"; 8 | import { InjectType } from "@halsp/inject"; 9 | import NavigatorMiddleware from "./middlewares/navigator.middleware"; 10 | 11 | export default function (startup: T): T { 12 | return startup 13 | .use(async (ctx, next) => { 14 | ctx.res.set("version", (await getVersion()) ?? ""); 15 | await next(); 16 | }) 17 | .useEnv() 18 | .useInject() 19 | .inject(CollectionService, InjectType.Singleton) 20 | .inject(CbappService, InjectType.Singleton) 21 | .useConsoleLogger() 22 | .use(async (ctx, next) => { 23 | const logger = await ctx.getLogger(); 24 | logger.info("event: " + JSON.stringify(ctx.lambdaEvent)); 25 | logger.info("context: " + JSON.stringify(ctx.lambdaContext)); 26 | await next(); 27 | }) 28 | .add(NavigatorMiddleware); 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 hal-wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /cloudbaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "envId": "{{env.ENV_ID}}", 4 | "$schema": "https://framework-1258016615.tcloudbaseapp.com/schema/latest.json", 5 | "functionRoot": "./functions", 6 | "framework": { 7 | "name": "navigation", 8 | "hooks": { 9 | "preDeploy": { 10 | "type": "execCommand", 11 | "commands": [ 12 | "npm i -g yarn --force && yarn config set registry https://registry.npmjs.org", 13 | "yarn install && yarn build" 14 | ] 15 | } 16 | }, 17 | "plugins": { 18 | "function": { 19 | "use": "@cloudbase/framework-plugin-function", 20 | "inputs": { 21 | "functionRootPath": "dist", 22 | "functions": [ 23 | { 24 | "name": "navigation", 25 | "envVariables": { 26 | "DEFAULT_URL": "{{env.DEFAULT_URL}}" 27 | }, 28 | "runtime": "Nodejs12.16", 29 | "memorySize": 256 30 | } 31 | ] 32 | } 33 | }, 34 | "db": { 35 | "use": "@cloudbase/framework-plugin-database", 36 | "inputs": { 37 | "collections": [ 38 | { 39 | "collectionName": "navigation" 40 | } 41 | ] 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # navigation 2 | 3 | 用于自定义域名跳转,无需服务器,一键搭建 4 | 5 | 可以任意域名跳转到任意链接 6 | 7 | 也可以让域名返回特定消息 8 | 9 | 一键部署: 10 | [![](https://main.qcloudimg.com/raw/67f5a389f1ac6f3b4d04c7256438e44f.svg)](https://console.cloud.tencent.com/tcb/env/index?action=CreateAndDeployCloudBaseProject&appUrl=https%3A%2F%2Fgithub.com%2Fhal-wang%2Fnavigation&branch=main) 11 | 12 | ## 配置 13 | 14 | ### 配置默认链接 15 | 16 | 添加环境变量 `DEFAULT_URL`,值为默认链接 17 | 18 | GET 请求找不到跳转目标时,会跳转到默认链接,否则返回 404 19 | 20 | ### 添加访问服务 21 | 22 | 添加想要跳转的域名至访问服务 23 | 24 | - 触发路径为 `/` 25 | - 关联资源为云函数 `navigation` 26 | 27 | ### 添加跳转 28 | 29 | 在云数据库中的集合 `navigation` 添加文档 30 | 31 | ```JSON 32 | { 33 | "_id": "domain", 34 | "to": "to url", 35 | "type": 1, 36 | "code": "status code" 37 | } 38 | ``` 39 | 40 | - \_id: 要跳转的域名,通过这个域名访问将会跳转请求,与前面访问服务添加的域名相同 41 | - to: 跳转目标,内容与 type 有关 42 | - type: 类型 43 | - 0 域名跳转,to 是域名,会传递路由参数 44 | - 1 路径跳转,跳转到 to 指定的路径 45 | - 2 返回消息,结构为 `{"message":"to"}` 46 | - 3 返回消息,内容是 to 47 | - code: 跳转的状态码 48 | 49 | - type 为 0,1 时,状态码 `code` 应为 `30x`,如 `301`,`302`,`306`,`307` 等 50 | 51 | ## 二次开发 52 | 53 | 如果现有功能不能满足,你可以进行二次开发 54 | 55 | ### 本地运行 56 | 57 | 在项目下创建文件 `.env.local`,内容如下 58 | 59 | ``` 60 | ENV_ID=cloudbase环境id 61 | SECRET_KEY=腾讯云 secret key 62 | SECRET_ID=腾讯云 secret id 63 | DEFAULT_URL=默认跳转链接 64 | ``` 65 | 66 | 安装依赖,在项目下执行 67 | 68 | ```sh 69 | npm install 70 | ``` 71 | 72 | 再使用 vscode 打开项目,直接 F5 开始调试 73 | 74 | 或在项目下执行 75 | 76 | ```sh 77 | npm start 78 | ``` 79 | 80 | ### 发布 81 | 82 | 可以本地使用 `@cloudbase/cli` 发布,也可以使用 GitHub Actions 持续集成 83 | 84 | #### cli 发布 85 | 86 | 确保项目根目录下有文件 `.env.local`,内容包含 87 | 88 | ``` 89 | ENV_ID=cloudbase环境id 90 | DEFAULT_URL=默认链接 91 | ``` 92 | 93 | 在项目根目录下运行以下命令发布 94 | 95 | ```sh 96 | npm install @cloudbase/cli -g 97 | npm run deploy 98 | ``` 99 | 100 | #### GitHub Actions 101 | 102 | 仓库增加 Secrets,在 `Settings -> Secrets -> Actions`,点击 `New repository secret` 按钮 103 | 104 | 增加如下记录 105 | 106 | - TENCENT_SECRET_ID: 腾讯云 secret id 107 | - TENCENT_SECRET_KEY: 腾讯云 secret key 108 | - ENV: 与 `cli 发布` 的 `.env.local` 文件内容相同 109 | 110 | 配置完成后,每次 main 分支提交代码就会自动发布到 CloudBase 111 | 112 | 发布进度可在仓库 `Actions` 中看到 113 | -------------------------------------------------------------------------------- /src/middlewares/navigator.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "@halsp/core"; 2 | import { Inject } from "@halsp/inject"; 3 | import { NavigationEntity } from "../entities/navigation.entity"; 4 | import { NavTypeEnum } from "../enums/nav-type.enum"; 5 | import { CollectionService } from "../services/collection.service"; 6 | 7 | export default class NavigatorMiddleware extends Middleware { 8 | @Inject 9 | private readonly collectionService!: CollectionService; 10 | 11 | async invoke(): Promise { 12 | const host = this.ctx.req.headers.host as string; 13 | if (!host) { 14 | this.redirectDefault(); 15 | return; 16 | } 17 | 18 | const res = await this.collectionService.navigation.doc(host).get(); 19 | const nav = res.data[0] as NavigationEntity; 20 | if (!nav) { 21 | this.redirectDefault(); 22 | return; 23 | } 24 | 25 | if (nav.type == NavTypeEnum.message || nav.type == NavTypeEnum.rootMsg) { 26 | this.ctx.res 27 | .setStatus(nav.code || 400) 28 | .setBody( 29 | nav.type == NavTypeEnum.rootMsg ? nav.to : { message: nav.to } 30 | ); 31 | } else { 32 | this.redirect( 33 | nav.type == NavTypeEnum.url ? nav.to : this.getDomainUrl(nav), 34 | nav.code || 307 35 | ); 36 | } 37 | } 38 | 39 | redirectDefault(): void { 40 | if (!this.ctx.req.method || this.ctx.req.method == "GET") { 41 | if (process.env.DEFAULT_URL) { 42 | this.redirect(process.env.DEFAULT_URL, 301); 43 | } else { 44 | this.notFoundMsg(); 45 | } 46 | } else { 47 | this.notFoundMsg(); 48 | } 49 | } 50 | 51 | getDomainUrl(nav: NavigationEntity): string { 52 | let domain = nav.to; 53 | const proto = this.ctx.req.getHeader("x-forwarded-proto"); 54 | const path = this.ctx.req.path; 55 | if (!domain.startsWith("http://") && !domain.startsWith("https://")) { 56 | domain = `${proto}://${domain}`; 57 | } 58 | 59 | let url = `${domain}/${path}`; 60 | if (this.ctx.req.query && Object.keys(this.ctx.req.query).length) { 61 | Object.keys(this.ctx.req.query).forEach((key, index) => { 62 | const value = encodeURIComponent(this.ctx.req.query[key] as string); 63 | url += `${index == 0 ? "?" : "&"}${key}=${value}`; 64 | }); 65 | } 66 | return url; 67 | } 68 | } 69 | --------------------------------------------------------------------------------