├── config.ts ├── src ├── resolvers │ ├── index.ts │ ├── query.ts │ └── mutation.ts ├── util │ ├── errors.ts │ ├── v2rayAndroid.ts │ ├── generateBase64.ts │ ├── index.ts │ ├── authentication.ts │ ├── v2rayIOS.ts │ ├── ssr.ts │ └── routers.ts ├── schema.ts ├── entities │ ├── subscribe.ts │ ├── user.ts │ └── node.ts ├── app.ts └── migration │ └── 1543503641364-default.ts ├── ormconfig.json ├── scripts └── make_release ├── tsconfig.json ├── Dockerfile ├── README.md ├── package.json ├── .gitignore └── api └── api.graphql /config.ts: -------------------------------------------------------------------------------- 1 | export const SALT_ROUNDS = 10; 2 | export const JWT_SECRET = "skr_skr_skr"; 3 | -------------------------------------------------------------------------------- /src/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import * as Mutation from "./mutation"; 2 | import * as Query from "./query" 3 | 4 | export const resolvers = { 5 | Query, 6 | Mutation 7 | } 8 | -------------------------------------------------------------------------------- /src/util/errors.ts: -------------------------------------------------------------------------------- 1 | 2 | import { GraphQLError } from "graphql"; 3 | 4 | export function validationError(errors) { 5 | return new GraphQLError( 6 | "无法处理请求!", 7 | undefined, 8 | undefined, 9 | undefined, 10 | undefined, 11 | undefined, 12 | { errorFields: errors } 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/schema.ts: -------------------------------------------------------------------------------- 1 | 2 | import { resolvers } from "./resolvers"; 3 | import { makeExecutableSchema } from "graphql-tools"; 4 | import * as fs from "fs" 5 | import * as path from "path" 6 | 7 | const schemaPath = path.join(__filename, "..", "..", "api", "api.graphql") 8 | const typeDefs = fs.readFileSync(schemaPath, "utf8") 9 | export const schema = makeExecutableSchema({ 10 | typeDefs, 11 | resolvers 12 | }) 13 | 14 | -------------------------------------------------------------------------------- /ormconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "postgres", 3 | "host": "localhost", 4 | "port": 5432, 5 | "username": "test", 6 | "password": "test", 7 | "database": "test", 8 | "synchronize": true, 9 | "logging": true, 10 | "entities": [ 11 | "./src/entities/*.ts" 12 | ], 13 | "migrations": [ 14 | "src/migration/**/*.ts" 15 | ], 16 | "subscribers": [ 17 | "src/subscriber/**/*.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /scripts/make_release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git diff --quiet HEAD 4 | # if [ "$?" -ne "0" ]; then 5 | # echo "Local repo is not clean!" 6 | # exit 1 7 | # fi 8 | 9 | set -ex 10 | 11 | rm -rf lib 12 | yarn tsc 13 | 14 | sudo docker build . -f Dockerfile -t registry-intl.ap-southeast-1.aliyuncs.com/one_subscribe/subscribe:$1 15 | sudo docker push registry-intl.ap-southeast-1.aliyuncs.com/one_subscribe/subscribe:$1 16 | 17 | # git tag -a $1 -m "$1" 18 | # git push origin $1 -------------------------------------------------------------------------------- /src/util/ v2rayAndroid.ts: -------------------------------------------------------------------------------- 1 | export function v2rayAndroid(nodes) { 2 | let v2rayServers = '' 3 | for(let node of nodes) { 4 | if(node.type == 'V2RAY') { 5 | node.info.v = "2" 6 | // node.info.path = node.info.path.replace(/\//, '') 7 | delete node.info.method 8 | let baseV2ray = Buffer.from(JSON.stringify(node.info)).toString('base64') 9 | let server = Buffer.from('vmess://' + baseV2ray) 10 | v2rayServers = v2rayServers + server + '\n' 11 | } 12 | } 13 | return v2rayServers 14 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es5", 5 | "es6", 6 | "dom", 7 | "esnext.asynciterable", 8 | "es2017" 9 | ], 10 | "target": "es6", 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "outDir": "lib", 16 | "sourceMap": true 17 | }, 18 | "include": [ 19 | "src/**/*", 20 | "configs/**/*" 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:9.6 2 | MAINTAINER PengChujin 3 | 4 | WORKDIR /code 5 | 6 | COPY package.json /code/package.json 7 | COPY yarn.lock /code/yarn.lock 8 | 9 | RUN yarn install --production 10 | 11 | COPY api/api.graphql /code/api/api.graphql 12 | COPY config/ormconfig.json /code/ormconfig.json 13 | COPY lib/src /code/src 14 | COPY config/clash.yml /code/src/util 15 | COPY lib/config.js /code/config.js 16 | COPY lib/config.js.map /code/config.js.map 17 | 18 | ENV NODE_ENV production 19 | EXPOSE 3001 20 | CMD ["node", "src/app.js"] 21 | -------------------------------------------------------------------------------- /src/util/generateBase64.ts: -------------------------------------------------------------------------------- 1 | import { v2rayAndroid } from './ v2rayAndroid' 2 | import { v2rayIOS } from './v2rayIOS' 3 | import { ssr } from './ssr' 4 | export function generateBase64(nodes, client){ 5 | let a = [] 6 | let v2rayServers = '' 7 | let ssrServers = '' 8 | // android 9 | if(client == 'Android' || client == 'Curl') { 10 | v2rayServers = v2rayAndroid(nodes) 11 | } 12 | // iOS 13 | else { 14 | v2rayServers = v2rayIOS(nodes) 15 | } 16 | ssrServers = ssr(nodes) 17 | return Buffer.from(v2rayServers + ssrServers).toString('base64') 18 | } -------------------------------------------------------------------------------- /src/entities/subscribe.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | Index, 6 | OneToMany, 7 | ManyToMany, 8 | ManyToOne, 9 | JoinTable 10 | } from "typeorm"; 11 | import { Node } from "./node"; 12 | import { User } from "./user"; 13 | 14 | @Entity() 15 | export class Subscribe { 16 | 17 | @PrimaryGeneratedColumn("uuid") 18 | id: string; 19 | 20 | @Column() 21 | name: String 22 | 23 | @ManyToMany(type => Node, node => node.subscribes, {eager: true}) 24 | @JoinTable() 25 | nodes: Node[] 26 | 27 | @ManyToOne(subscribe => User, user => user.subscribes) 28 | user: User 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/entities/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | Index, 6 | OneToMany 7 | } from "typeorm"; 8 | import { Node } from "./node"; 9 | import { Subscribe } from "./subscribe"; 10 | 11 | @Entity("user") 12 | export class User { 13 | @PrimaryGeneratedColumn("uuid") 14 | id: string; 15 | 16 | @Column({ 17 | length: "20" 18 | }) 19 | @Index({ unique: true }) 20 | username: string; 21 | 22 | @Column("text") encryptedPassword: string; 23 | 24 | @OneToMany(type => Node, node => node.user) 25 | nodes: Node[] 26 | 27 | @OneToMany(type => Subscribe, subscribe => subscribe.user) 28 | subscribes: Subscribe[] 29 | } 30 | -------------------------------------------------------------------------------- /src/entities/node.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | ManyToMany, 6 | ManyToOne, 7 | JoinTable 8 | } from "typeorm" 9 | import { User } from "./user"; 10 | import { Subscribe } from "./subscribe"; 11 | 12 | @Entity() 13 | export class Node { 14 | 15 | @PrimaryGeneratedColumn() 16 | id: Number 17 | 18 | @Column("enum", { enum: ["SSR", "V2RAY", "SS"], default: "SS" }) 19 | type: String 20 | 21 | @Column('jsonb') 22 | info: any 23 | 24 | @Column({ default: 0 }) 25 | serial: Number 26 | 27 | @ManyToOne(node => User, user => user.nodes) 28 | user: User 29 | 30 | @ManyToMany(node => Subscribe, subcribe => subcribe.nodes) 31 | subscribes: Subscribe[] 32 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 干啥的 3 | 4 | * 什么是订阅 5 | 6 | 大家代理一般用的 ss/r 或者 V2ray,一个节点。当你自己使用多个节点的时候问题就来了,如何方便管理、获取自己的节点。这个时候SSR的作者就提出了订阅的这种方式。第一 url 地址里面存储所有节点信息。每次客户端填写下这个 url 地址就行了。目前 Clash(Win 和 Mac),iOS的 Shadowrocket小火箭、Quantumult 圈, 安卓的 SSR v2rayN、 Win 的 ssr、以及梅林等路由器固件都支持了。非常方便,然后也方便分享。当然如果你是买的机场服务,你肯定已经使用过了。 7 | 8 | * 关于我写的这个 9 | 10 | 注册后可以添加 ss/ssr 的节点, 在订阅管理可以看到各种订阅的地址,以及二维码。节点 添加、删除、修改、都是可以的。 11 | 12 | ## 网站和开源 13 | 14 | * 节点的内容是放在我的数据库的,我不开机场,自己的节点也完全够用,我肯定不会窃取你的数据。不放心完全可以自己来全是开源的。 15 | * 后端开源地址 [Github](https://github.com/pengchujin/oneSubscribe) 16 | * 前端开源地址 [Github](https://github.com/pengchujin/subscribeVue) 17 | * 网站地址 [sebs.club](https://sebs.club) 18 | 19 | ## 附加 20 | 21 | 推荐大家使用 v2ray tls+ws 这种方式。 可以看下我写的这个docker,使用很方便。[V2rayDocker](https://github.com/pengchujin/v2rayDocker) -------------------------------------------------------------------------------- /src/resolvers/query.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../entities/user" 2 | import { Node } from "../entities/node" 3 | import { Subscribe } from "../entities/subscribe" 4 | import * as R from "ramda" 5 | import { ensureUser } from "../util/authentication" 6 | 7 | export async function nodesList(_obj, { }, { db, jwt}) { 8 | const user = await ensureUser(db, jwt) 9 | const repository = db.getRepository(Node) 10 | let nodes = await repository.find({where: {user: user}, order: { 11 | id: "DESC" 12 | }}) 13 | 14 | return nodes 15 | } 16 | 17 | export async function subscribeList(_obj, { }, { db, jwt}) { 18 | const user = await ensureUser(db, jwt) 19 | const repository = db.getRepository(Subscribe) 20 | let subscribeList = await repository.find({user: user}) 21 | return subscribeList 22 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscribebackend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "ts-node src/app.ts" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "apollo-server-koa": "^2.1.0", 14 | "axios": "^0.18.0", 15 | "bcrypt": "^3.0.2", 16 | "graphql": "^14.0.2", 17 | "graphql-playground-html": "^1.6.4", 18 | "js-md5": "^0.7.3", 19 | "js-yaml": "^3.12.1", 20 | "json2yaml": "^1.1.0", 21 | "jsonwebtoken": "^8.3.0", 22 | "koa": "^2.6.1", 23 | "koa-graphql": "^0.7.5", 24 | "koa-mount": "^4.0.0", 25 | "koa-useragent": "^1.2.0", 26 | "koa2-useragent": "^0.3.1", 27 | "pg": "^7.6.0", 28 | "ramda": "^0.25.0", 29 | "ts-node": "^7.0.1", 30 | "typeorm": "^0.2.9", 31 | "typescript": "^3.1.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'koa' 2 | import { 3 | MiddlewareOptions, 4 | renderPlaygroundPage, 5 | RenderPageOptions, 6 | } from 'graphql-playground-html' 7 | 8 | /* tslint:disable-next-line */ 9 | const { playgroundVersion } = require('../../package.json') 10 | 11 | export type KoaPlaygroundMiddleware = (ctx: Context, next: () => void) => void 12 | 13 | export type Register = (options: MiddlewareOptions) => KoaPlaygroundMiddleware 14 | 15 | const koa: Register = options => { 16 | const middlewareOptions: RenderPageOptions = { 17 | ...options, 18 | version: playgroundVersion, 19 | } 20 | 21 | return async function voyager(ctx, next) { 22 | try { 23 | ctx.body = await renderPlaygroundPage(middlewareOptions) 24 | await next() 25 | } catch (err) { 26 | ctx.body = { message: err.message } 27 | ctx.status = err.status || 500 28 | } 29 | } 30 | } 31 | 32 | export default koa 33 | -------------------------------------------------------------------------------- /src/util/authentication.ts: -------------------------------------------------------------------------------- 1 | 2 | import { sign, verify } from "jsonwebtoken"; 3 | import * as config from "../../config"; 4 | import { validationError } from "./errors"; 5 | import { User } from "../entities/user"; 6 | 7 | function extractJwt(jwt) { 8 | const parts = (jwt || "").split(" "); 9 | if (parts.length !== 2) { 10 | return null; 11 | } 12 | try { 13 | return verify(parts[1], config.JWT_SECRET); 14 | } catch (err) { 15 | return null; 16 | } 17 | } 18 | 19 | export async function fetchUser(db, jwt) { 20 | const jwtObject = extractJwt(jwt); 21 | if (!(jwtObject && typeof jwtObject === "object" && jwtObject.id)) { 22 | return null; 23 | } 24 | const repository = db.getRepository(User); 25 | return await repository.findOne({ id: jwtObject.id }); 26 | } 27 | 28 | export async function ensureUser(db, jwt) { 29 | const user = await fetchUser(db, jwt); 30 | if (!user) { 31 | throw validationError({ 32 | jwt: ["请先登录!"] 33 | }); 34 | } 35 | return user; 36 | } 37 | -------------------------------------------------------------------------------- /src/util/v2rayIOS.ts: -------------------------------------------------------------------------------- 1 | export function v2rayIOS(nodes) { 2 | let servers = '' 3 | for(let node of nodes) { 4 | if(node.type == 'V2RAY') { 5 | !node.info.method ? node.info.method = 'chacha20-poly1305' : '' 6 | let v2rayBase = '' + node.info.method + ':' + node.info.id + '@' + node.info.add + ':' + node.info.port 7 | let remarks = '' 8 | // let obfsParam = '' 9 | let path = '' 10 | let obfs = '' 11 | let tls = '' 12 | !node.info.ps ? remarks = 'remarks=oneSubscribe' : remarks = `remarks=${node.info.ps}` 13 | !node.info.path ? '' : path = `&path=${node.info.path}` 14 | node.info.net == 'ws' ? obfs = `&obfs=websocket` : '' 15 | node.info.net == 'h2' ? obfs = `&obfs=http` : '' 16 | node.info.tls == 'tls' ? tls = `&tls=1` : '' 17 | let query = remarks + path + obfs + tls 18 | let baseV2ray = Buffer.from(v2rayBase).toString('base64') 19 | let server = Buffer.from('vmess://' + baseV2ray + '?' + query) 20 | servers = servers + server + '\n' 21 | } 22 | } 23 | return servers 24 | } -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from 'koa' 2 | const cors = require('@koa/cors'); 3 | const bodyParser = require('koa-bodyparser'); 4 | import * as Router from 'koa-router' 5 | const mount = require('koa-mount'); 6 | import routers from './util/routers' 7 | const userAgent = require('koa-useragent') 8 | import * as graphqlHTTP from 'koa-graphql'; 9 | import { createConnection } from "typeorm" 10 | import { schema } from './schema'; 11 | const koaPlayground = require('./util/index').default 12 | 13 | 14 | const bootstrap = async () => { 15 | const app = new Koa() 16 | const db = await createConnection(); 17 | app.use(bodyParser()) 18 | app.use(cors()) 19 | app.use(userAgent) 20 | const router = new Router() 21 | app.use(mount('/graphql', graphqlHTTP((ctx, next) => { 22 | return { 23 | schema, 24 | context: { 25 | db: db, 26 | graphiql: true, 27 | ctx: ctx, 28 | jwt: ctx.req.headers.authorization, 29 | } 30 | } 31 | }))) 32 | router.all( '/playground', 33 | koaPlayground({ 34 | endpoint: '/graphql', 35 | })) 36 | app.use(routers.routes()) 37 | app.use(router.routes()) 38 | app.use(router.allowedMethods()) 39 | app.listen(3001) 40 | } 41 | 42 | bootstrap() 43 | -------------------------------------------------------------------------------- /src/util/ssr.ts: -------------------------------------------------------------------------------- 1 | export function ssr(nodes){ 2 | let a = [] 3 | let ssrServers = '' 4 | for (let i of nodes) { 5 | if(i.type == 'SSR') { 6 | a.push(i.info) 7 | } 8 | } 9 | for (let i in a) { 10 | let proto = '' 11 | if(a[i].obfs == "none" || a[i].obfs == "") { 12 | a[i].obfs = 'plain' 13 | } 14 | if(a[i].proto != "none" || a[i].obfs != "" || a[i].obfs != "origin") { 15 | // a[i].proto = 'plain' 16 | proto = '&' + 'protoparam=' 17 | } 18 | let remarks = Buffer.from(a[i].title).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '') 19 | let base64Pw = Buffer.from(a[i].password).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '') 20 | let group = Buffer.from("ONESubscribe").toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '') 21 | let url = '' + a[i].host + ':' + a[i].port + ':' + a[i].proto + ':' + a[i].method 22 | + ':' + a[i].obfs + ':' + base64Pw + '/?' + 'obfsparam=' 23 | // + a[i].obfsParam 24 | + proto 25 | // + a[i].protoParam 26 | + '&' + 'remarks=' + remarks + '&' + 'group=' + group 27 | // + '&udpport=0&uot=0' 28 | let baseSSR = Buffer.from(url).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '') 29 | let server = Buffer.from('ssr://' + baseSSR) 30 | ssrServers = ssrServers + server + '\n' 31 | } 32 | return ssrServers 33 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .node_modules/ 3 | built/* 4 | tests/cases/rwc/* 5 | tests/cases/test262/* 6 | tests/cases/perf/* 7 | !tests/cases/webharness/compilerToString.js 8 | test-args.txt 9 | ~*.docx 10 | \#*\# 11 | .\#* 12 | tests/baselines/local/* 13 | tests/baselines/local.old/* 14 | tests/services/baselines/local/* 15 | tests/baselines/prototyping/local/* 16 | tests/baselines/rwc/* 17 | tests/baselines/test262/* 18 | tests/baselines/reference/projectOutput/* 19 | tests/baselines/local/projectOutput/* 20 | tests/baselines/reference/testresults.tap 21 | tests/services/baselines/prototyping/local/* 22 | tests/services/browser/typescriptServices.js 23 | src/harness/*.js 24 | src/compiler/diagnosticInformationMap.generated.ts 25 | src/compiler/diagnosticMessages.generated.json 26 | src/parser/diagnosticInformationMap.generated.ts 27 | src/parser/diagnosticMessages.generated.json 28 | rwc-report.html 29 | *.swp 30 | build.json 31 | *.actual 32 | tests/webTestServer.js 33 | tests/webTestServer.js.map 34 | tests/webhost/*.d.ts 35 | tests/webhost/webtsc.js 36 | tests/cases/**/*.js 37 | tests/cases/**/*.js.map 38 | *.config 39 | scripts/debug.bat 40 | scripts/run.bat 41 | scripts/word2md.js 42 | scripts/buildProtocol.js 43 | scripts/ior.js 44 | scripts/authors.js 45 | scripts/configurePrerelease.js 46 | scripts/open-user-pr.js 47 | scripts/processDiagnosticMessages.d.ts 48 | scripts/processDiagnosticMessages.js 49 | scripts/produceLKG.js 50 | scripts/importDefinitelyTypedTests/importDefinitelyTypedTests.js 51 | scripts/generateLocalizedDiagnosticMessages.js 52 | scripts/*.js.map 53 | scripts/typings/ 54 | coverage/ 55 | internal/ 56 | **/.DS_Store 57 | .settings 58 | **/.vs 59 | **/.vscode 60 | !**/.vscode/tasks.json 61 | !tests/cases/projects/projectOption/**/node_modules 62 | !tests/cases/projects/NodeModulesSearch/**/* 63 | !tests/baselines/reference/project/nodeModules*/**/* 64 | .idea 65 | yarn.lock 66 | yarn-error.log 67 | .parallelperf.* 68 | tests/cases/user/*/package-lock.json 69 | tests/cases/user/*/node_modules/ 70 | tests/cases/user/*/**/*.js 71 | tests/cases/user/*/**/*.js.map 72 | tests/cases/user/*/**/*.d.ts 73 | !tests/cases/user/zone.js/ 74 | !tests/cases/user/bignumber.js/ 75 | !tests/cases/user/discord.js/ 76 | tests/baselines/reference/dt 77 | .failed-tests 78 | TEST-results.xml 79 | package-lock.json 80 | config/ 81 | lib/ -------------------------------------------------------------------------------- /api/api.graphql: -------------------------------------------------------------------------------- 1 | enum nodeType { 2 | SS, 3 | SSR, 4 | V2RAY 5 | } 6 | 7 | input SSR { 8 | title: String! 9 | host: String! 10 | method: String! 11 | flag: String 12 | obfs: String! 13 | obfsParam: String! 14 | proto: String! 15 | protoParam: String! 16 | port: Int! 17 | password: String! 18 | } 19 | 20 | input V2RAY { 21 | add: String!, 22 | ps: String!, 23 | port: String!, 24 | aid: String!, 25 | host: String!, 26 | id: String!, 27 | method: String!, 28 | net: String!, 29 | path: String!, 30 | tls: String!, 31 | type: String! 32 | } 33 | 34 | type NodeInfo { 35 | add: String, 36 | ps: String, 37 | port: String, 38 | aid: String, 39 | # host: String!, 40 | id: String, 41 | # method: String!, 42 | net: String, 43 | path: String, 44 | tls: String, 45 | type: String, 46 | title: String, 47 | host: String, 48 | method: String, 49 | flag: String, 50 | obfs: String, 51 | obfsParam: String, 52 | proto: String, 53 | protoParam: String, 54 | # port: Int! 55 | password: String 56 | } 57 | 58 | type Subscribe { 59 | name: String 60 | nodes: [NodeList] 61 | # uuid as the urlKey 62 | id: String 63 | } 64 | 65 | type User { 66 | id: ID! 67 | username: String! 68 | encryptedPassword: String 69 | } 70 | 71 | type NodeList { 72 | id: Int 73 | type: String, 74 | info: NodeInfo 75 | } 76 | 77 | type loginUser { 78 | id: ID! 79 | username: String! 80 | jwt: String 81 | } 82 | 83 | type Code { 84 | id: ID! 85 | code: Int 86 | } 87 | 88 | type Message { 89 | TF: String 90 | Message: String 91 | } 92 | 93 | type Query { 94 | users: [User] 95 | jwt: loginUser 96 | # Todo 97 | nodesList: [NodeList] 98 | subscribeList: [Subscribe] 99 | } 100 | 101 | type Mutation { 102 | signin(username: String!, password: String!): loginUser! 103 | signup(username: String!, password: String!): Message! 104 | addNode(type: nodeType!, nodeInfo: SSR! ): Message! 105 | addV2rayNode(type: nodeType!, nodeInfo: V2RAY! ): Message! 106 | createSubscribe(nodes: [Int], name: String!): Message! 107 | # Todo 108 | deleteNode(nodeID: Int): Message! 109 | modifyNode(nodeID: Int, nodeInfo: SSR): Message! 110 | modifyV2rayNode(nodeID: Int, nodeInfo: V2RAY): Message! 111 | modifySubscribe(id: String, name: String, nodes: [Int]): Message! 112 | getSubscribe(urlKey: String!, client: String): String! 113 | getAllNodes(urlKey: String!, client: String, type: nodeType): String! 114 | getClashX(urlKey: String!): [NodeList] 115 | # cPassword(username: String!, oPassword: String!, nPassword: String ): Modify 116 | # dUser(username: String!): Modify! 117 | } 118 | -------------------------------------------------------------------------------- /src/migration/1543503641364-default.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class default1543503641364 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.query(`CREATE TABLE "subscribe" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "userId" uuid, CONSTRAINT "PK_3e91e772184cd3feb30688ef1b8" PRIMARY KEY ("id"))`); 7 | await queryRunner.query(`CREATE TABLE "user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "username" character varying(20) NOT NULL, "encryptedPassword" text NOT NULL, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`); 8 | await queryRunner.query(`CREATE UNIQUE INDEX "IDX_78a916df40e02a9deb1c4b75ed" ON "user" ("username") `); 9 | await queryRunner.query(`CREATE TYPE "node_type_enum" AS ENUM('SSR', 'V2RAY', 'SS')`); 10 | await queryRunner.query(`CREATE TABLE "node" ("id" SERIAL NOT NULL, "type" "node_type_enum" NOT NULL DEFAULT 'SS', "info" jsonb NOT NULL, "serial" integer NOT NULL DEFAULT 0, "userId" uuid, CONSTRAINT "PK_8c8caf5f29d25264abe9eaf94dd" PRIMARY KEY ("id"))`); 11 | await queryRunner.query(`CREATE TABLE "subscribe_nodes_node" ("subscribeId" uuid NOT NULL, "nodeId" integer NOT NULL, CONSTRAINT "PK_1a8bb4946e1da27bdadd7ec93f6" PRIMARY KEY ("subscribeId", "nodeId"))`); 12 | await queryRunner.query(`ALTER TABLE "subscribe" ADD CONSTRAINT "FK_78138550e21d8b67790d761148d" FOREIGN KEY ("userId") REFERENCES "user"("id")`); 13 | await queryRunner.query(`ALTER TABLE "node" ADD CONSTRAINT "FK_49e3f89e68914252136980d77ac" FOREIGN KEY ("userId") REFERENCES "user"("id")`); 14 | await queryRunner.query(`ALTER TABLE "subscribe_nodes_node" ADD CONSTRAINT "FK_5431255c89447391dcbb89c547c" FOREIGN KEY ("subscribeId") REFERENCES "subscribe"("id") ON DELETE CASCADE`); 15 | await queryRunner.query(`ALTER TABLE "subscribe_nodes_node" ADD CONSTRAINT "FK_bfe7ead80efc6fdba7761b745b1" FOREIGN KEY ("nodeId") REFERENCES "node"("id") ON DELETE CASCADE`); 16 | } 17 | 18 | public async down(queryRunner: QueryRunner): Promise { 19 | await queryRunner.query(`ALTER TABLE "subscribe_nodes_node" DROP CONSTRAINT "FK_bfe7ead80efc6fdba7761b745b1"`); 20 | await queryRunner.query(`ALTER TABLE "subscribe_nodes_node" DROP CONSTRAINT "FK_5431255c89447391dcbb89c547c"`); 21 | await queryRunner.query(`ALTER TABLE "node" DROP CONSTRAINT "FK_49e3f89e68914252136980d77ac"`); 22 | await queryRunner.query(`ALTER TABLE "subscribe" DROP CONSTRAINT "FK_78138550e21d8b67790d761148d"`); 23 | await queryRunner.query(`DROP TABLE "subscribe_nodes_node"`); 24 | await queryRunner.query(`DROP TABLE "node"`); 25 | await queryRunner.query(`DROP TYPE "node_type_enum"`); 26 | await queryRunner.query(`DROP INDEX "IDX_78a916df40e02a9deb1c4b75ed"`); 27 | await queryRunner.query(`DROP TABLE "user"`); 28 | await queryRunner.query(`DROP TABLE "subscribe"`); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/util/routers.ts: -------------------------------------------------------------------------------- 1 | import * as Router from 'koa-router' 2 | import axios from 'axios' 3 | import * as fs from 'fs' 4 | import * as yaml from 'js-yaml' 5 | import * as YAML from 'json2yaml' 6 | const routers = new Router() 7 | 8 | let url = 'http://127.0.0.1:3001/graphql' 9 | 10 | routers.get('/allnodes/:uuid', async (ctx, next) => { 11 | let client = ctx.userAgent.platform 12 | let params = ctx.params.uuid 13 | let response = await axios.post(url, { 14 | query: ` 15 | mutation{ getAllNodes(urlKey: "${params}", client: "${client}")} 16 | ` 17 | }) 18 | ctx.body = response.data.data.getAllNodes + '' 19 | }) 20 | 21 | routers.get('/v2ray/:uuid', async (ctx, next) => { 22 | let client = ctx.userAgent.platform 23 | let params = ctx.params.uuid 24 | let response = await axios.post(url, { 25 | query: ` 26 | mutation{ getAllNodes(urlKey: "${params}", client: "${client}", type: V2RAY)} 27 | ` 28 | }) 29 | ctx.body = response.data.data.getAllNodes + '' 30 | }) 31 | 32 | routers.get('/ssr/:uuid', async (ctx, next) => { 33 | let client = ctx.userAgent.platform 34 | let params = ctx.params.uuid 35 | let response = await axios.post(url, { 36 | query: ` 37 | mutation{ getAllNodes(urlKey: "${params}", client: "${client}", type: SSR)} 38 | ` 39 | }) 40 | ctx.body = response.data.data.getAllNodes + '' 41 | }) 42 | 43 | routers.get('/ClashX/:uuid', async (ctx, next) => { 44 | let params = ctx.params.uuid 45 | let response = await axios.post(url, { 46 | query: ` 47 | mutation{ getClashX(urlKey: "${params}") { 48 | id type info { add ps aid id net path tls type title port host method obfs obfsParam proto protoParam password } 49 | } } 50 | ` 51 | }) 52 | try { 53 | var doc = yaml.safeLoad(fs.readFileSync(__dirname +'/clash.yml', 'utf8')); 54 | } catch (e) { 55 | console.log(e); 56 | } 57 | let nodes = response.data.data.getClashX 58 | let group =[] 59 | for(let i of nodes) { 60 | if(i.type == "SSR") { 61 | let ss = { 62 | name: i.info.title, 63 | type: 'ss', 64 | server: i.info.host, 65 | port: parseInt(i.info.port), 66 | cipher: i.info.method, 67 | password: i.info.password } 68 | doc.Proxy.push(ss) 69 | group.push(i.info.title) 70 | } else if(i.type == "V2RAY") { 71 | let vmess = { 72 | name: i.info.ps, 73 | type: "vmess", 74 | server: i.info.add, 75 | port: parseInt(i.info.port), 76 | uuid: i.info.id, 77 | alterId: parseInt(i.info.aid), 78 | cipher: "auto" } 79 | if(i.info.tls == "tls") { 80 | if (i.info.net != "ws") { 81 | vmess["tls"] = true 82 | } else { 83 | vmess["network"] = "ws" 84 | vmess["tls"] = true 85 | vmess["ws-path"] = i.info.path 86 | } 87 | } else { 88 | if (i.info.net == "ws") { 89 | vmess["network"] = "ws" 90 | vmess["ws-path"] = i.info.path 91 | vmess["ws-headers"] = { 92 | Host: i.info.host 93 | } 94 | } 95 | } 96 | doc.Proxy.push(vmess) 97 | group.push(i.info.ps) 98 | } 99 | } 100 | let group2 = group.concat('auto') 101 | doc["Proxy Group"][0].proxies = group 102 | doc["Proxy Group"][1].proxies = group 103 | doc["Proxy Group"][2].proxies = group2 104 | let res = YAML.stringify(doc) 105 | ctx.body = res 106 | }) 107 | 108 | export default routers -------------------------------------------------------------------------------- /src/resolvers/mutation.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../entities/user" 2 | import { Node } from "../entities/node" 3 | import { Subscribe } from "../entities/subscribe" 4 | import * as bcrypt from "bcrypt" 5 | import * as Jwt from "jsonwebtoken" 6 | import * as config from "../../config" 7 | import { generateBase64 } from "../util/generateBase64" 8 | import { validationError } from "../util/errors" 9 | import { ensureUser } from "../util/authentication" 10 | import { from } from "apollo-link"; 11 | let Message = { 12 | TF: '', 13 | Message: "OK" 14 | } 15 | // import * as R from "ramda" 16 | export async function signup(_obj, { username, password }, { db }) { 17 | const repository = db.getRepository(User); 18 | 19 | const hash = bcrypt.hashSync(password, config.SALT_ROUNDS); 20 | let b = { 21 | encryptedPassword: hash, 22 | username: username 23 | } 24 | try { 25 | await repository.save(b) 26 | } catch(err) { 27 | return { 28 | TF: 'error', 29 | Message: '注册失败,邮箱已被注册' 30 | } 31 | } 32 | return { 33 | TF: 'success', 34 | Message: '注册成功请登录' 35 | } 36 | } 37 | 38 | function authenticate(user, password) { 39 | if (!user) { 40 | return false 41 | } else { 42 | return bcrypt.compareSync(password, user.encryptedPassword) 43 | } 44 | } 45 | 46 | export async function signin(_obj, { username, password }, { db }) { 47 | let a = { jwt: String, id: Number, username: String } 48 | const repository = db.getRepository(User); 49 | const userSaved = await repository.findOne({ username: username }) 50 | let TF = authenticate(userSaved, password) 51 | if (TF) { 52 | let userToken = { 53 | username: username, 54 | id: userSaved.id 55 | } 56 | const jwt = Jwt.sign(userToken, config.JWT_SECRET, { expiresIn: '30d' }) 57 | a.jwt = jwt 58 | a.username = username 59 | a.id = userSaved.id 60 | } else { 61 | throw validationError({ 62 | LoginError: ["用户名,或者密码错误"] 63 | }); 64 | } 65 | return a 66 | } 67 | 68 | export async function addNode(_obj, { type, nodeInfo }, { db, jwt }) { 69 | const user = await ensureUser(db, jwt) 70 | const nodeRepository = db.getRepository(Node); 71 | const node = nodeRepository.create({ 72 | type: type, 73 | info: nodeInfo, 74 | user: user 75 | }) 76 | await nodeRepository.save(node) 77 | return { 78 | TF: 'success', 79 | Message: `节点 ${nodeInfo.title} 已添加成功 ` 80 | } 81 | } 82 | 83 | export async function addV2rayNode(_obj, { type, nodeInfo }, { db, jwt }) { 84 | const user = await ensureUser(db, jwt) 85 | const nodeRepository = db.getRepository(Node); 86 | const node = nodeRepository.create({ 87 | type: type, 88 | info: nodeInfo, 89 | user: user 90 | }) 91 | await nodeRepository.save(node) 92 | return { 93 | TF: 'success', 94 | Message: `节点 ${nodeInfo.ps} 已添加成功 ` 95 | } 96 | } 97 | 98 | export async function createSubscribe(_obj, { nodes, name }, { db, jwt }) { 99 | const user = await ensureUser(db, jwt) 100 | const nodeRepository = db.getRepository(Node); 101 | const subscribeRepository = db.getRepository(Subscribe) 102 | if(!nodes) { 103 | let subscribeNodes = await nodeRepository.find({user: user}) 104 | subscribeRepository.save({ 105 | name: name, 106 | nodes: subscribeNodes, 107 | user: user 108 | }) 109 | } else { 110 | let subscribeNodes = [] 111 | for (let node of nodes) { 112 | let subscribeNode = await nodeRepository.findOne({user: user, id: node}) 113 | subscribeNodes.push(subscribeNode) 114 | } 115 | subscribeRepository.save({ 116 | name: name, 117 | nodes: subscribeNodes, 118 | user: user 119 | }) 120 | } 121 | return { 122 | TF: 'success', 123 | Message: "订阅创建成功" 124 | } 125 | } 126 | 127 | export async function modifyNode(_obj, { nodeID, nodeInfo }, { db, jwt}) { 128 | const user = await ensureUser(db, jwt) 129 | const nodeRepository = db.getRepository(Node); 130 | let node = await nodeRepository.findOne({user: user, id: nodeID }) 131 | await nodeRepository.update(node, {info: nodeInfo}) 132 | return { 133 | TF: 'success', 134 | Message: `节点 ${nodeInfo.title} 已修改 ` 135 | } 136 | } 137 | 138 | export async function modifyV2rayNode(_obj, { nodeID, nodeInfo }, { db, jwt}) { 139 | const user = await ensureUser(db, jwt) 140 | const nodeRepository = db.getRepository(Node); 141 | let node = await nodeRepository.findOne({user: user, id: nodeID }) 142 | await nodeRepository.update(node, {info: nodeInfo}) 143 | return { 144 | TF: 'success', 145 | Message: `节点 ${nodeInfo.ps} 已修改 ` 146 | } 147 | } 148 | 149 | export async function modifySubscribe(_obj, { id, name, nodes }, { db, jwt }){ 150 | const user = await ensureUser(db, jwt) 151 | const subscribeRepository = db.getRepository(Subscribe) 152 | const nodeRepository = db.getRepository(Node); 153 | let subscribe = await subscribeRepository.findOne({user: user, id: id}) 154 | let subscribeNodes = [] 155 | for (let node of nodes) { 156 | let subscribeNode = await nodeRepository.findOne({user: user, id: node}) 157 | subscribeNodes.push(subscribeNode) 158 | } 159 | subscribe.nodes = subscribeNodes 160 | subscribe.name = name 161 | await subscribeRepository.save(subscribe) 162 | return Message 163 | } 164 | export async function deleteNode(_obj, { nodeID }, { db, jwt }) { 165 | const user = await ensureUser(db, jwt) 166 | const nodeRepository = db.getRepository(Node); 167 | let node = await nodeRepository.findOne({user: user, id: nodeID }) 168 | await nodeRepository.delete(node) 169 | let info = '' 170 | if(!node.info.title){ 171 | info = node.info.ps 172 | } else { 173 | info = node.info.title 174 | } 175 | return { 176 | TF: 'success', 177 | Message: `节点 ${info} 已删除 ` 178 | } 179 | } 180 | 181 | 182 | export async function getSubscribe(_obj, { urlKey, client}, { db, jwt }) { 183 | const subscribeRepository = db.getRepository(Subscribe) 184 | let subscribe = await subscribeRepository.findOne({id: urlKey}) 185 | let nodes = subscribe.nodes 186 | return generateBase64(nodes, client) 187 | } 188 | 189 | export async function getAllNodes(_obj, { urlKey, client, type },ctx){ 190 | const nodeRepository = ctx.db.getRepository(Node); 191 | const userRepository = ctx.db.getRepository(User) 192 | let user = await userRepository.findOne({id: urlKey}) 193 | let nodes = [] 194 | if(!type) { 195 | nodes = await nodeRepository.find({user: user}) 196 | } else { 197 | nodes = await nodeRepository.find({user: user, type: type}) 198 | } 199 | return generateBase64(nodes, client) 200 | } 201 | 202 | export async function getClashX(_obj, { urlKey },ctx) { 203 | const repository = ctx.db.getRepository(Node) 204 | const userRepository = ctx.db.getRepository(User) 205 | let user = await userRepository.findOne({id: urlKey}) 206 | let nodes = await repository.find({where: {user: user}, order: { 207 | id: "DESC" 208 | }}) 209 | return nodes 210 | } 211 | --------------------------------------------------------------------------------