From 992db80ea327517890882867d4c24b826e3e94aa Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Thu, 5 Mar 2026 12:21:18 +0300 Subject: [PATCH] initial commit --- .env | 5 ++ .gitignore | 2 + Dockerfile | 14 +++++ Makefile | 3 + app/api.go | 79 ++++++++++++++++++++++++++ app/config.go | 13 +++++ app/database.go | 44 +++++++++++++++ app/manager.go | 81 +++++++++++++++++++++++++++ app/nodes.go | 32 +++++++++++ app/routes.go | 125 +++++++++++++++++++++++++++++++++++++++++ app/types.go | 133 ++++++++++++++++++++++++++++++++++++++++++++ app/utils.go | 31 +++++++++++ go.mod | 12 ++++ go.sum | 24 ++++++++ main.go | 19 +++++++ scripts/00-init.sql | 6 ++ 16 files changed, 623 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 app/api.go create mode 100644 app/config.go create mode 100644 app/database.go create mode 100644 app/manager.go create mode 100644 app/nodes.go create mode 100644 app/routes.go create mode 100644 app/types.go create mode 100644 app/utils.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 scripts/00-init.sql diff --git a/.env b/.env new file mode 100644 index 0000000..6f6764a --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +PSQL_HOST=127.0.0.1 +PSQL_NAME=hyst2 +PSQL_USER=hyst2 +PSQL_PASS=123456 +JWT_SECRET=CHANGEME \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aeed1ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +provider.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6d4c7be --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.26-alpine AS builder +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY ./app ./app/ +COPY main.go . +RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg/mod go build -v -o api . + +FROM alpine:3.23 AS runner +WORKDIR /app +COPY --from=builder /src/api /app/api +EXPOSE 8080 +CMD ["/app/api"] +USER nobody \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b985a52 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +build: + docker build -t git.nix13.pw/scuroneko/h2master:latest . + docker push git.nix13.pw/scuroneko/h2master:latest \ No newline at end of file diff --git a/app/api.go b/app/api.go new file mode 100644 index 0000000..de00669 --- /dev/null +++ b/app/api.go @@ -0,0 +1,79 @@ +package app + +import ( + "encoding/json" + "io" + "log" + "net/http" +) + +type Error struct { + Ok bool `json:"ok"` + Error string `json:"error"` +} + +func WriteError(w http.ResponseWriter, err error) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + e := json.NewEncoder(w).Encode(Error{ + Ok: false, + Error: err.Error(), + }) + if e != nil { + log.Println(e) + } +} + +type Response[T any] struct { + Ok bool `json:"ok"` + Data T `json:"data"` +} + +func WriteResponse[T any](w http.ResponseWriter, data T) { + WriteResponseCode(w, data, 200) +} +func WriteResponseCode[T any](w http.ResponseWriter, data T, code int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + enc := json.NewEncoder(w) + enc.SetEscapeHTML(false) + e := enc.Encode(Response[T]{ + Ok: true, + Data: data, + }) + if e != nil { + log.Println(e) + } +} + +func WriteResponseOrError[T any](w http.ResponseWriter, data T, err error) { + if err != nil { + WriteError(w, err) + } else { + WriteResponse(w, data) + } +} +func ReadBody[T any](r io.ReadCloser) (T, error) { + dst := new(T) + data, err := io.ReadAll(r) + if err != nil { + return *dst, err + } + err = json.Unmarshal(data, dst) + return *dst, err +} +func ReadResponse[T any](r io.ReadCloser) (T, error) { + var zero T + resp, err := ReadBody[Response[T]](r) + if err != nil { + return zero, err + } + return resp.Data, nil +} +func CheckToken(r *http.Request) bool { + auth := r.Header.Get("Authorization") + if auth != cfg.JWTSecret { + return false + } + return true +} diff --git a/app/config.go b/app/config.go new file mode 100644 index 0000000..4ab04ae --- /dev/null +++ b/app/config.go @@ -0,0 +1,13 @@ +package app + +import "os" + +type Config struct { + JWTSecret string +} + +var cfg *Config + +func LoadConfig() { + cfg = &Config{JWTSecret: os.Getenv("JWT_SECRET")} +} diff --git a/app/database.go b/app/database.go new file mode 100644 index 0000000..737da30 --- /dev/null +++ b/app/database.go @@ -0,0 +1,44 @@ +package app + +import ( + "fmt" + "log" + "os" + + "github.com/joho/godotenv" + _ "github.com/lib/pq" + "github.com/vinovest/sqlx" +) + +var db *sqlx.DB = nil + +func init() { + if err := godotenv.Load(); err != nil { + log.Println("Error loading .env file. If you use docker, then you can ignore this.") + } + + if db == nil { + connectPsql() + } +} + +func getDSN() string { + user := os.Getenv("PSQL_USER") + password := os.Getenv("PSQL_PASS") + database := os.Getenv("PSQL_NAME") + host, exists := os.LookupEnv("PSQL_HOST") + if !exists { + host = "localhost" + } + return fmt.Sprintf("postgresql://%s:%s@%s/%s?sslmode=disable", user, password, host, database) +} +func connectPsql() { + var err error + db, err = sqlx.Connect("postgres", getDSN()) + if err != nil { + panic(err) + } +} +func ClosePsql() error { + return db.Close() +} diff --git a/app/manager.go b/app/manager.go new file mode 100644 index 0000000..0e8544b --- /dev/null +++ b/app/manager.go @@ -0,0 +1,81 @@ +package app + +import ( + "fmt" + "io" + "net/http" + "strings" + "time" +) + +type NodeManager struct { + client *http.Client + token string + url string +} + +func NewNodeManager(node Node) NodeManager { + client := &http.Client{Timeout: time.Second * 15} + return NodeManager{ + client, node.Token, node.URL, + } +} +func (m *NodeManager) NewRequest(method, path string, body io.Reader) (*http.Request, error) { + req, err := http.NewRequest(method, fmt.Sprintf("%s%s", m.url, path), body) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", m.token) + return req, nil +} +func (m *NodeManager) Do(req *http.Request) (*http.Response, error) { + return m.client.Do(req) +} +func (m *NodeManager) Request(method, path string, body io.Reader) (*http.Response, error) { + req, err := m.NewRequest(method, path, body) + if err != nil { + return nil, err + } + return m.Do(req) +} +func (m *NodeManager) LoadConfig() (NodeConfig, HysteriaConfig, error) { + var _cfg NodeConfig + var hysteriaCfg HysteriaConfig + + resp, err := m.Request("GET", "/config", nil) + if err != nil { + return _cfg, hysteriaCfg, err + } + defer resp.Body.Close() + data, err := ReadResponse[NodeConfigRsp](resp.Body) + if err != nil { + return _cfg, hysteriaCfg, err + } + return data.Config, data.HysteriaConfig, nil +} +func (m *NodeManager) AddUser(username, password string) (User, error) { + body := strings.NewReader(fmt.Sprintf(`{"username":"%s","password":"%s"}`, username, password)) + resp, err := m.Request("POST", "/users", body) + if err != nil { + return User{}, err + } + defer resp.Body.Close() + return ReadResponse[User](resp.Body) +} +func (m *NodeManager) GetUser(id uint64) (User, error) { + var user User + resp, err := m.Request("GET", fmt.Sprintf("/users/%d", id), nil) + if err != nil { + return user, err + } + defer resp.Body.Close() + return ReadResponse[User](resp.Body) +} +func (m *NodeManager) GetUserKey(id uint64) (string, error) { + resp, err := m.Request("GET", fmt.Sprintf("/users/%d/key", id), nil) + if err != nil { + return "", err + } + defer resp.Body.Close() + return ReadResponse[string](resp.Body) +} diff --git a/app/nodes.go b/app/nodes.go new file mode 100644 index 0000000..39b162e --- /dev/null +++ b/app/nodes.go @@ -0,0 +1,32 @@ +package app + +import ( + "github.com/vinovest/sqlx" +) + +type NodeRepository struct { + db *sqlx.DB +} + +func NewNodeRepository(db *sqlx.DB) *NodeRepository { return &NodeRepository{db} } + +func (r *NodeRepository) All() ([]Node, error) { + nodes := make([]Node, 0) + sql := "SELECT * FROM nodes;" + err := r.db.Select(&nodes, sql) + return nodes, err +} +func (r *NodeRepository) Find(id int) (Node, error) { + var node Node + sql := "SELECT * FROM nodes WHERE id=?;" + sql = r.db.Rebind(sql) + err := r.db.Select(&node, sql, id) + return node, err +} +func (r *NodeRepository) Create(token, url string) (Node, error) { + sql := "INSERT INTO nodes (token, url) VALUES (?, ?) RETURNING *;" + sql = r.db.Rebind(sql) + var node Node + err := r.db.Get(&node, sql, token, url) + return node, err +} diff --git a/app/routes.go b/app/routes.go new file mode 100644 index 0000000..a152cc5 --- /dev/null +++ b/app/routes.go @@ -0,0 +1,125 @@ +package app + +import ( + "encoding/base64" + "log" + "net/http" + "strconv" + "strings" + + "github.com/go-chi/chi/v5" +) + +var hosts = []string{ + "185.231.245.25", + "46.243.6.125", +} + +func AddNode(w http.ResponseWriter, r *http.Request) { + type AddNodeReq struct { + Token string `json:"token"` + URL string `json:"url"` + } + req, err := ReadBody[AddNodeReq](r.Body) + if err != nil { + log.Println(err) + } + rep := NewNodeRepository(db) + node, err := rep.Create(req.Token, req.URL) + WriteResponseOrError(w, node, err) +} +func GetNodes(w http.ResponseWriter, r *http.Request) { + rep := NewNodeRepository(db) + nodes, err := rep.All() + WriteResponseOrError(w, nodes, err) +} +func GetNodeById(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + nodeId, err := strconv.Atoi(id) + if err != nil { + WriteError(w, err) + return + } + rep := NewNodeRepository(db) + node, err := rep.Find(nodeId) + WriteResponseOrError(w, node, err) +} + +func AddUser(w http.ResponseWriter, r *http.Request) { + rep := NewNodeRepository(db) + nodes, err := rep.All() + if err != nil { + WriteError(w, err) + return + } + + type AddUserReq struct { + Username string `json:"username"` + Password string `json:"password"` + } + req, err := ReadBody[AddUserReq](r.Body) + if err != nil { + WriteError(w, err) + return + } + + var user User + for _, node := range nodes { + manager := NewNodeManager(node) + user, err = manager.AddUser(req.Username, req.Password) + if err != nil { + WriteError(w, err) + return + } + } + WriteResponseOrError(w, user, err) +} +func Sub(w http.ResponseWriter, r *http.Request) { + idS := chi.URLParam(r, "id") + userId, err := strconv.ParseUint(idS, 10, 32) + if err != nil { + log.Println(err) + return + } + + rep := NewNodeRepository(db) + nodes, err := rep.All() + if err != nil { + WriteError(w, err) + return + } + + subs := make([]string, 0) + for _, node := range nodes { + manager := NewNodeManager(node) + conf, hystConf, err := manager.LoadConfig() + if err != nil { + WriteError(w, err) + continue + } + key, err := manager.GetUserKey(userId) + if err != nil { + WriteError(w, err) + return + } + user, err := manager.GetUser(userId) + if err != nil { + WriteError(w, err) + return + } + hystUrl := buildSubUrl(conf, hystConf, key, user) + subs = append(subs, hystUrl) + } + + sub := []byte(base64.StdEncoding.EncodeToString([]byte(strings.Join(subs, "\n")))) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Content-Length", strconv.Itoa(len(sub))) + w.Header().Set("Content-Disposition", `attachment; filename="ScuroNeko"`) + w.Header().Set("profile-web-page-url", "http://185.231.245.25:9997/sub/1") + w.Header().Set("support-url", "https://t.me/scuroneko") + w.Header().Set("profile-title", "base64:YmFuLm5peDEzLnB3") + w.Header().Set("profile-update-interval", "12") + w.Header().Set("subscription-userinfo", "upload=0; download=197782510450; total=0; expire=0") + w.WriteHeader(200) + w.Write(sub) +} diff --git a/app/types.go b/app/types.go new file mode 100644 index 0000000..f1e2087 --- /dev/null +++ b/app/types.go @@ -0,0 +1,133 @@ +package app + +type User struct { + ID int `json:"id"` + Username string `json:"username"` + Password string `json:"password"` +} + +type Node struct { + ID int `json:"id"` + Token string `json:"token"` + URL string `json:"url"` +} + +type NodeConfig struct { + JWTSecret string + Host string + SNI string + NameFormat string +} + +type HysteriaTLSConfig struct { + Cert string `yaml:"cert"` + Key string `yaml:"key"` +} + +type HysteriaObfsConfig struct { + Type string `yaml:"type"` + Salamander struct { + Password string `yaml:"password"` + } `yaml:"salamander"` +} + +type HysteriaQuicConfig struct { + InitStreamReceiveWindow *int64 `yaml:"initStreamReceiveWindow"` + MaxStreamReceiveWindow *int64 `yaml:"maxStreamReceiveWindow"` + InitConnReceiveWindow *int64 `yaml:"initConnReceiveWindow"` + MaxConnReceiveWindow *int64 `yaml:"maxConnReceiveWindow"` + MaxIdleTimeout *string `yaml:"maxIdleTimeout"` + MaxIncomingStream *int64 `yaml:"maxIncomingStream"` + DisablePathMTUDiscovery *bool `yaml:"disablePathMTUDiscovery"` +} + +type HysteriaBandwidthConfig struct { + UP *string `yaml:"up"` + Down *string `yaml:"down"` + IgnoreClientBandwidth *bool `yaml:"ignoreClientBandwidth"` +} + +type HysteriaAuthType string +type HysteriaAuthConfig struct { + Type HysteriaAuthType `yaml:"type"` + Password string `yaml:"password,omitempty"` + Userpass map[string]string `yaml:"userpass,omitempty"` + Http struct { + URL string `yaml:"url"` + Insecure bool `yaml:"insecure"` + } `yaml:"http,omitempty"` + Command string `yaml:"command,omitempty"` +} + +type HysteriaTCPUDPResolverConfig struct { + Address string `yaml:"addr"` + Timeout string `yaml:"timeout"` +} +type HysteriaTLSResolverConfig struct { + Address string `yaml:"addr"` + Timeout string `yaml:"timeout"` + SNI string `yaml:"sni"` + Insecure bool `yaml:"insecure"` +} +type HysteriaResolverType string +type HysteriaResolverConfig struct { + Type HysteriaResolverType `yaml:"type"` + TCP *HysteriaTCPUDPResolverConfig `yaml:"tcp,omitempty"` + UDP *HysteriaTCPUDPResolverConfig `yaml:"udp,omitempty"` + TLS *HysteriaTLSResolverConfig `yaml:"tls,omitempty"` + HTTPS *HysteriaTLSResolverConfig `yaml:"https,omitempty"` +} + +type HysteriaSniffConfig struct { + Enable bool `yaml:"enable"` +} + +type HysteriaACLConfig struct{} +type HysteriaOutboundConfig struct { + Name string `yaml:"name"` + Type string `yaml:"type"` +} + +type HysteriaTrafficStatsConfig struct { + Listen string `yaml:"listen"` + Secret string `yaml:"secret"` +} + +type HysteriaMasqueradeType string +type HysteriaMasqueradeFile struct { + Dir string `yaml:"dir"` +} +type HysteriaMasqueradeProxy struct { + URL string `yaml:"url"` + RewriteHost *bool `yaml:"rewriteHost,omitempty"` + Insecure *bool `yaml:"insecure,omitempty"` +} +type HysteriaMasqueradeString struct { + Content string `yaml:"content"` + Headers map[string]string `yaml:"headers,omitempty"` + StatusCode int `yaml:"statusCode"` +} +type HysteriaMasqueradeConfig struct { + Type HysteriaMasqueradeType `yaml:"type"` + File *HysteriaMasqueradeFile `yaml:"file,omitempty"` + Proxy *HysteriaMasqueradeProxy `yaml:"proxy,omitempty"` + String *HysteriaMasqueradeString `yaml:"string,omitempty"` + + ListenHTTP *string `yaml:"listenHTTP,omitempty"` + ListenHTTPS *string `yaml:"listenHTTPS,omitempty"` + ForceHTTPS *bool `yaml:"forceHTTPS,omitempty"` +} + +type HysteriaConfig struct { + Listen string `yaml:"listen"` + TLS HysteriaTLSConfig `yaml:"tls"` + Obfs *HysteriaObfsConfig `yaml:"obfs,omitempty"` + Quic *HysteriaQuicConfig `yaml:"quic,omitempty"` + SpeedTest *bool `yaml:"speedTest,omitempty"` + Auth HysteriaAuthConfig `yaml:"auth"` + Resolver *HysteriaResolverConfig `yaml:"resolver,omitempty"` + Sniff *HysteriaSniffConfig `yaml:"sniff,omitempty"` + ACL *HysteriaACLConfig `yaml:"acl,omitempty"` + Outbounds []HysteriaOutboundConfig `yaml:"outbounds,omitempty"` + TrafficStats *HysteriaTrafficStatsConfig `yaml:"trafficStats,omitempty"` +} diff --git a/app/utils.go b/app/utils.go new file mode 100644 index 0000000..8c3e07c --- /dev/null +++ b/app/utils.go @@ -0,0 +1,31 @@ +package app + +import ( + "fmt" + "net/url" + "strings" +) + +type NodeConfigRsp struct { + Config NodeConfig `json:"config"` + HysteriaConfig HysteriaConfig `json:"hysteria"` +} + +func buildSubUrl(conf NodeConfig, hystConf HysteriaConfig, key string, user User) string { + values := url.Values{} + values.Add("type", "hysteria") + values.Add("security", "tls") + values.Add("alpn", "h3,h2") + values.Add("fp", "chrome") + values.Add("allowInsecure", "0") + values.Add("sni", conf.SNI) + if hystConf.Obfs != nil { + values.Add("obfs", hystConf.Obfs.Type) + values.Add("obfs-password", hystConf.Obfs.Salamander.Password) + } + + host := fmt.Sprintf("%s@%s%s", key, conf.Host, hystConf.Listen) + subName := strings.ReplaceAll(conf.NameFormat, "{username}", user.Username) + return fmt.Sprintf("hysteria2://%s?%s#%s", host, values.Encode(), subName) + // hysteria2://pass@185.231.245.25:51052?&type=hysteria&mport&security=tls&sni=nix13.pw&alpn=h3&fp=chrome&allowInsecure=0#🇷🇺ssia (user1) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..00a2bb8 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module Hyst2API + +go 1.26 + +require ( + github.com/go-chi/chi/v5 v5.2.5 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.11.2 + github.com/vinovest/sqlx v1.7.2 +) + +require github.com/muir/sqltoken v0.1.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a96efbc --- /dev/null +++ b/go.sum @@ -0,0 +1,24 @@ +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= +github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= +github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/muir/sqltoken v0.1.0 h1:edosEGsOClOZNfgGQNQSgxR9O6LiVefm2rDRqp2InuI= +github.com/muir/sqltoken v0.1.0/go.mod h1:lgOIORnKekMsuc/ZwdPOfwz/PtWLPCke43cEbT3uDuY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vinovest/sqlx v1.7.2 h1:t/IahCJqO71GJYnhOcACiUXlMiiMomMHtxtUthdcBfo= +github.com/vinovest/sqlx v1.7.2/go.mod h1:o49uG4W/ZYZompljKx5GZ7qx6OFklPjSHXP63nSmND8= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ea4f45c --- /dev/null +++ b/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "Hyst2API/app" + "log" + "net/http" + + "github.com/go-chi/chi/v5" +) + +func main() { + r := chi.NewRouter() + r.Post("/nodes", app.AddNode) + r.Get("/nodes", app.GetNodes) + r.Get("/nodes/{id}", app.GetNodeById) + r.Post("/users", app.AddUser) + r.Get("/sub/{id}", app.Sub) + log.Fatal(http.ListenAndServe(":9997", r)) +} diff --git a/scripts/00-init.sql b/scripts/00-init.sql new file mode 100644 index 0000000..3f4a802 --- /dev/null +++ b/scripts/00-init.sql @@ -0,0 +1,6 @@ +CREATE TABLE nodes( + id SERIAL PRIMARY KEY, + token TEXT NOT NULL, + url TEXT NOT NULL +); +CREATE UNIQUE INDEX nodes_id ON nodes(id); \ No newline at end of file