commit 7fad5ac9e42939c2a5a027de0a8c42d01e49b4b7 Author: Andre Medeiros Date: Sun May 9 20:51:15 2021 -0400 initial pass diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47760cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +tmp/ +.env diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..8c8ab9a --- /dev/null +++ b/Brewfile @@ -0,0 +1,9 @@ +tap 'contribsys/faktory' + +brew 'faktory' +brew 'foreman' +brew 'golang' +brew 'golang-migrate' +brew 'postgres' +brew 'redis' + diff --git a/Brewfile.lock.json b/Brewfile.lock.json new file mode 100644 index 0000000..61d6ea2 --- /dev/null +++ b/Brewfile.lock.json @@ -0,0 +1,177 @@ +{ + "entries": { + "brew": { + "postgres": { + "version": "13.2_2", + "bottle": { + "rebuild": 0, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_big_sur": { + "cellar": "/opt/homebrew/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/postgresql/blobs/sha256:5bb80b319443cc57a44a9a9a8507a5255728d2caf359ef7e931c79c2c833b900", + "sha256": "5bb80b319443cc57a44a9a9a8507a5255728d2caf359ef7e931c79c2c833b900" + }, + "big_sur": { + "cellar": "/usr/local/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/postgresql/blobs/sha256:ee460f1f8beb4d877121faf4ebcbf8069f86a8ef4ae05fa342bc2fcdde75bb47", + "sha256": "ee460f1f8beb4d877121faf4ebcbf8069f86a8ef4ae05fa342bc2fcdde75bb47" + }, + "catalina": { + "cellar": "/usr/local/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/postgresql/blobs/sha256:1f50565ab7a703d85ecc37179e8d461dc82de4e3e6266ba085dd19a0b95ed5db", + "sha256": "1f50565ab7a703d85ecc37179e8d461dc82de4e3e6266ba085dd19a0b95ed5db" + }, + "mojave": { + "cellar": "/usr/local/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/postgresql/blobs/sha256:c0a5828cb5caef09e7b4acd99b450b6d4ceb3a0d265a95b147e63e23fb6f7596", + "sha256": "c0a5828cb5caef09e7b4acd99b450b6d4ceb3a0d265a95b147e63e23fb6f7596" + } + } + } + }, + "golang": { + "version": "1.16.3", + "bottle": { + "rebuild": 0, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_big_sur": { + "cellar": "/opt/homebrew/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:e7c1efdd09e951eb46d01a3200b01e7fa55ce285b75470051be7fef34f4233ce", + "sha256": "e7c1efdd09e951eb46d01a3200b01e7fa55ce285b75470051be7fef34f4233ce" + }, + "big_sur": { + "cellar": "/usr/local/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:ea37f33fd27369612a3e4e6db6adc46db0e8bdf6fac1332bf51bafaa66d43969", + "sha256": "ea37f33fd27369612a3e4e6db6adc46db0e8bdf6fac1332bf51bafaa66d43969" + }, + "catalina": { + "cellar": "/usr/local/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:69c28f5e60612801c66e51e93d32068f822b245ab83246cb6cb374572eb59e15", + "sha256": "69c28f5e60612801c66e51e93d32068f822b245ab83246cb6cb374572eb59e15" + }, + "mojave": { + "cellar": "/usr/local/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:bf1e90ed1680b8ee1acb49f2f99426c8a8ac3e49efd63c7f3b41e57e7214dd19", + "sha256": "bf1e90ed1680b8ee1acb49f2f99426c8a8ac3e49efd63c7f3b41e57e7214dd19" + } + } + } + }, + "foreman": { + "version": "0.87.2", + "bottle": { + "rebuild": 0, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/foreman/blobs/sha256:575f9fbc16eca16cf479196ce44d87bb817ddb1e2eed59869ffe158d98d08a9f", + "sha256": "575f9fbc16eca16cf479196ce44d87bb817ddb1e2eed59869ffe158d98d08a9f" + }, + "big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/foreman/blobs/sha256:70c762dd642d8f5aa3ca5a28e420b6c9f7befaf7699de073b7d62e174fdee88f", + "sha256": "70c762dd642d8f5aa3ca5a28e420b6c9f7befaf7699de073b7d62e174fdee88f" + }, + "catalina": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/foreman/blobs/sha256:5c2b39c1f7e9667b9ebc6b7228b6cf31f06c2261c85019028272cfdda7073ea5", + "sha256": "5c2b39c1f7e9667b9ebc6b7228b6cf31f06c2261c85019028272cfdda7073ea5" + }, + "mojave": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/foreman/blobs/sha256:674b5fc005986f47294acedccba6b2a2bcdc1d423e392a356f8d58cc88a2c81a", + "sha256": "674b5fc005986f47294acedccba6b2a2bcdc1d423e392a356f8d58cc88a2c81a" + }, + "high_sierra": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/foreman/blobs/sha256:b0d289ff31caf33f3d549af6dd615e37588aadb243355395380c4df5b0e52d63", + "sha256": "b0d289ff31caf33f3d549af6dd615e37588aadb243355395380c4df5b0e52d63" + } + } + } + }, + "faktory": { + "version": "1.5.1-1", + "bottle": false + }, + "redis": { + "version": "6.2.3", + "bottle": { + "rebuild": 0, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_big_sur": { + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/redis/blobs/sha256:b2b3cfeca2d5f110507e9e7a7af8918786f2853e39e49b0b39de68762e5b5030", + "sha256": "b2b3cfeca2d5f110507e9e7a7af8918786f2853e39e49b0b39de68762e5b5030" + }, + "big_sur": { + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/redis/blobs/sha256:d891c5b376746c3895098fd384fb4edba972b532848f63cbad5be20e611458ac", + "sha256": "d891c5b376746c3895098fd384fb4edba972b532848f63cbad5be20e611458ac" + }, + "catalina": { + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/redis/blobs/sha256:a269e87b26515775a7034d9d6cb996ed63d783b5a6d681b64bab92ce93bed55b", + "sha256": "a269e87b26515775a7034d9d6cb996ed63d783b5a6d681b64bab92ce93bed55b" + }, + "mojave": { + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/redis/blobs/sha256:3373d834552eef5f6c71889299124693de6b5d5b887e520d6db96ab51da81020", + "sha256": "3373d834552eef5f6c71889299124693de6b5d5b887e520d6db96ab51da81020" + } + } + } + }, + "golang-migrate": { + "version": "4.14.1", + "bottle": { + "rebuild": 0, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/golang-migrate/blobs/sha256:3565f7a03cfd1eeec3110aa8d56f03baa79b0de2718103c0095e51187ecd37ee", + "sha256": "3565f7a03cfd1eeec3110aa8d56f03baa79b0de2718103c0095e51187ecd37ee" + }, + "big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/golang-migrate/blobs/sha256:5c61a106d9970b0f9b14e78e1523894d57b50cd0473f7d5a1fb1a9161dbff159", + "sha256": "5c61a106d9970b0f9b14e78e1523894d57b50cd0473f7d5a1fb1a9161dbff159" + }, + "catalina": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/golang-migrate/blobs/sha256:a77af5282af35e0d073e82140b091eedf0b478c19aea36f1b06738690989cebb", + "sha256": "a77af5282af35e0d073e82140b091eedf0b478c19aea36f1b06738690989cebb" + }, + "mojave": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/golang-migrate/blobs/sha256:8fa3758e979f09c171388887c831a6518e3f8df67b07668b6c8cebf76b19a653", + "sha256": "8fa3758e979f09c171388887c831a6518e3f8df67b07668b6c8cebf76b19a653" + } + } + } + } + }, + "tap": { + "contribsys/faktory": { + "revision": "ada2f8b18fe79a40a906a7371d36c1d750422ca3" + } + } + }, + "system": { + "macos": { + "big_sur": { + "HOMEBREW_VERSION": "3.1.5", + "HOMEBREW_PREFIX": "/usr/local", + "Homebrew/homebrew-core": "17fb25009a4876e9369cb621e798b758eb0c7fe6", + "CLT": "12.5.0.0.1.1617976050", + "Xcode": "12.5", + "macOS": "11.3.1" + } + } + } +} diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..f0efab7 --- /dev/null +++ b/Procfile @@ -0,0 +1,4 @@ +postgres: postgres -D tmp/postgresql +faktory: faktory +server: go run github.com/andremedeiros/apollo/cmd/apollo-api +worker: go run github.com/andremedeiros/apollo/cmd/apollo-worker diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f698a5 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Apollo API + +## Getting started + +```sh +$ script/bootstrap +$ cp .env.example .env +``` + +## Running the server + +```sh +$ script/server +``` diff --git a/cmd/apollo-api/accounts.go b/cmd/apollo-api/accounts.go new file mode 100644 index 0000000..f6a220b --- /dev/null +++ b/cmd/apollo-api/accounts.go @@ -0,0 +1,62 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/julienschmidt/httprouter" + + "github.com/andremedeiros/apollo/internal/data" +) + +func (app *application) upsertAccountHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + a := &data.Account{} + if err := json.NewDecoder(r.Body).Decode(a); err != nil { + fmt.Println("failing on decoding json") + app.errorResponse(w, r, 500, err.Error()) + return + } + + a.ExpiresAt = time.Now().Unix() + 3300 + + // Here we check whether the account is supplied with a valid token. + ac := app.client.NewAuthenticatedClient(a.RefreshToken, a.AccessToken) + me, err := ac.Me() + + if err != nil { + fmt.Println("failing on fetching remote user") + app.errorResponse(w, r, 500, err.Error()) + return + } + + if me.NormalizedUsername() != a.NormalizedUsername() { + fmt.Println("failing on account username comparison") + app.errorResponse(w, r, 500, "nice try") + return + } + + // Upsert account + if err := app.models.Accounts.Upsert(a); err != nil { + fmt.Println("failing on account upsert") + app.errorResponse(w, r, 500, err.Error()) + return + } + + // Associate + d, err := app.models.Devices.GetByAPNSToken(ps.ByName("apns")) + if err != nil { + fmt.Println("failing on apns") + app.errorResponse(w, r, 500, err.Error()) + return + } + + if err := app.models.DevicesAccounts.Associate(a.ID, d.ID); err != nil { + fmt.Println("failing on associate") + app.errorResponse(w, r, 500, err.Error()) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/cmd/apollo-api/devices.go b/cmd/apollo-api/devices.go new file mode 100644 index 0000000..bb36105 --- /dev/null +++ b/cmd/apollo-api/devices.go @@ -0,0 +1,25 @@ +package main + +import ( + "encoding/json" + "net/http" + + "github.com/julienschmidt/httprouter" + + "github.com/andremedeiros/apollo/internal/data" +) + +func (app *application) upsertDeviceHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + d := &data.Device{} + if err := json.NewDecoder(r.Body).Decode(d); err != nil { + app.errorResponse(w, r, 500, err.Error()) + return + } + + if err := app.models.Devices.Upsert(d); err != nil { + app.errorResponse(w, r, 500, err.Error()) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/cmd/apollo-api/errors.go b/cmd/apollo-api/errors.go new file mode 100644 index 0000000..8120b4c --- /dev/null +++ b/cmd/apollo-api/errors.go @@ -0,0 +1,8 @@ +package main + +import "net/http" + +func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message string) { + w.WriteHeader(status) + w.Write([]byte(message)) +} diff --git a/cmd/apollo-api/health.go b/cmd/apollo-api/health.go new file mode 100644 index 0000000..3b33e50 --- /dev/null +++ b/cmd/apollo-api/health.go @@ -0,0 +1,18 @@ +package main + +import ( + "encoding/json" + "net/http" + + "github.com/julienschmidt/httprouter" +) + +func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + data := map[string]string{ + "status": "available", + } + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(data) +} diff --git a/cmd/apollo-api/main.go b/cmd/apollo-api/main.go new file mode 100644 index 0000000..81822c1 --- /dev/null +++ b/cmd/apollo-api/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "database/sql" + "flag" + "fmt" + "log" + "net/http" + "os" + + "github.com/andremedeiros/apollo/internal/data" + "github.com/andremedeiros/apollo/internal/reddit" + + faktory "github.com/contribsys/faktory/client" + "github.com/joho/godotenv" + _ "github.com/lib/pq" +) + +type config struct { + port int +} + +type application struct { + cfg config + logger *log.Logger + db *sql.DB + faktory *faktory.Client + models *data.Models + client *reddit.Client +} + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + var cfg config + flag.IntVar(&cfg.port, "port", 4000, "API server port") + flag.Parse() + + logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) + + db, err := sql.Open("postgres", os.Getenv("DATABASE_URL")) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + faktory, err := faktory.Open() + if err != nil { + log.Fatal(err) + } + defer faktory.Close() + + rc := reddit.NewClient(os.Getenv("REDDIT_CLIENT_ID"), os.Getenv("REDDIT_CLIENT_SECRET")) + + app := &application{ + cfg, + logger, + db, + faktory, + data.NewModels(db), + rc, + } + + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", cfg.port), + Handler: app.routes(), + } + + logger.Printf("starting server on %s", srv.Addr) + err = srv.ListenAndServe() + logger.Fatal(err) + + /* + rc := reddit.NewClient("C7MjYkx1czyRDA", "I2AsVWbrf8h4vdQxVa5Pvf84vScF1w") + rac := rc.NewAuthenticatedClient("2532458-kGp6OeR-LMoQNrUXRL-7UNfyBbViRA", "2532458-UE7IvJK3-VuTdMJB0bgMv58fxKQhww") + rac.MessageInbox() + */ +} diff --git a/cmd/apollo-api/routes.go b/cmd/apollo-api/routes.go new file mode 100644 index 0000000..bf9f4cc --- /dev/null +++ b/cmd/apollo-api/routes.go @@ -0,0 +1,16 @@ +package main + +import ( + "github.com/julienschmidt/httprouter" +) + +func (app *application) routes() *httprouter.Router { + router := httprouter.New() + + router.GET("/v1/health", app.healthCheckHandler) + + router.POST("/v1/device", app.upsertDeviceHandler) + router.POST("/v1/device/:apns/account", app.upsertAccountHandler) + + return router +} diff --git a/cmd/apollo-worker/main.go b/cmd/apollo-worker/main.go new file mode 100644 index 0000000..08f8a99 --- /dev/null +++ b/cmd/apollo-worker/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "database/sql" + "log" + "os" + + worker "github.com/contribsys/faktory_worker_go" + "github.com/joho/godotenv" + + "github.com/andremedeiros/apollo/internal/data" + "github.com/andremedeiros/apollo/internal/reddit" +) + +type application struct { + logger *log.Logger + db *sql.DB + models *data.Models + client *reddit.Client +} + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) + + db, err := sql.Open("postgres", os.Getenv("DATABASE_URL")) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + rc := reddit.NewClient(os.Getenv("REDDIT_CLIENT_ID"), os.Getenv("REDDIT_CLIENT_SECRET")) + + app := &application{ + logger, + db, + data.NewModels(db), + rc, + } + + mgr := worker.NewManager() + mgr.ProcessStrictPriorityQueues("critical", "default", "bulk") + mgr.Concurrency = 20 + mgr.Run() +} diff --git a/env.example b/env.example new file mode 100644 index 0000000..b770841 --- /dev/null +++ b/env.example @@ -0,0 +1,4 @@ +DATABASE_URL=postgres://apollo:@localhost/apollo?sslmode=disable +FAKTORY_URL=tcp://localhost:7419 +REDDIT_CLIENT_ID=C7MjYkx1czyRDA +REDDIT_CLIENT_SECRET=I2AsVWbrf8h4vdQxVa5Pvf84vScF1w diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bb44903 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/andremedeiros/apollo + +go 1.16 + +require ( + github.com/contribsys/faktory v1.5.1 + github.com/contribsys/faktory_worker_go v1.4.2 + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/joho/godotenv v1.3.0 + github.com/julienschmidt/httprouter v1.3.0 + github.com/lib/pq v1.10.1 + github.com/stretchr/testify v1.5.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bc314e3 --- /dev/null +++ b/go.sum @@ -0,0 +1,91 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/benbjohnson/ego v0.4.0/go.mod h1:6Qzj43pKX58WdlHDZKZS4Q/XXYwz6toIqXqNjzt4mNA= +github.com/contribsys/faktory v1.3.0-1/go.mod h1:qCp3bcPrIT7mb/aR5KendsK/Nyg7yE3JOg4sETojQN8= +github.com/contribsys/faktory v1.5.1 h1:Ac22RjbR8USVEwRV7jvz0+ytp2NquL5j7HC0IRM1K34= +github.com/contribsys/faktory v1.5.1/go.mod h1:f0dpwzksA7TmHquOcU9/j1NUUz8YNdx8LYDoNFIU9gw= +github.com/contribsys/faktory_worker_go v1.4.2 h1:IJD4wGk6iZSOkLEOHX42s6R8YvnQI2gEzN6WeZO9BBg= +github.com/contribsys/faktory_worker_go v1.4.2/go.mod h1:v53dJh0lrk18B/3zFJjAWM6/Lgq341sRricsBUZ/umc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= +github.com/go-redis/redis v6.15.7+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/justinas/nosurf v1.1.0/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= +github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo= +github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= +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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/data/accounts.go b/internal/data/accounts.go new file mode 100644 index 0000000..5d8dff4 --- /dev/null +++ b/internal/data/accounts.go @@ -0,0 +1,51 @@ +package data + +import ( + "database/sql" + "strings" +) + +type Account struct { + ID int64 + Username string + AccessToken string + RefreshToken string + ExpiresAt int64 + LastMessageID string + LastCheckedAt int64 +} + +func (a *Account) NormalizedUsername() string { + return strings.ToLower(a.Username) +} + +type AccountModel struct { + DB *sql.DB +} + +func (am *AccountModel) Upsert(a *Account) error { + query := ` + INSERT INTO accounts (username, access_token, refresh_token, expires_at, last_message_id, device_count, last_checked_at) + VALUES ($1, $2, $3, $4, '', 0, 0) + ON CONFLICT(username) + DO + UPDATE SET access_token = $2, refresh_token = $3, expires_at = $4, last_message_id = $5, last_checked_at = $6 + RETURNING id` + + args := []interface{}{a.NormalizedUsername(), a.AccessToken, a.RefreshToken, a.ExpiresAt, a.LastMessageID, a.LastCheckedAt} + return am.DB.QueryRow(query, args...).Scan(&a.ID) +} + +func (am *AccountModel) Delete(id int64) error { + return nil +} + +type MockAccountModel struct{} + +func (mam *MockAccountModel) Upsert(a *Account) error { + return nil +} + +func (mam *MockAccountModel) Delete(id int64) error { + return nil +} diff --git a/internal/data/device_accounts.go b/internal/data/device_accounts.go new file mode 100644 index 0000000..8cbc67b --- /dev/null +++ b/internal/data/device_accounts.go @@ -0,0 +1,42 @@ +package data + +import "database/sql" + +type DeviceAccount struct { + ID int64 + AccountID int64 + DeviceID int64 +} + +type DeviceAccountModel struct { + DB *sql.DB +} + +func (dam *DeviceAccountModel) Associate(accountID int64, deviceID int64) error { + query := ` + INSERT INTO devices_accounts (account_id, device_id) + VALUES ($1, $2) + ON CONFLICT (account_id, device_id) DO NOTHING + RETURNING id` + + args := []interface{}{accountID, deviceID} + if err := dam.DB.QueryRow(query, args...).Err(); err != nil { + return err + } + + // Update account counter + query = ` + UPDATE accounts + SET device_count = ( + SELECT COUNT(*) FROM devices_accounts WHERE account_id = $1 + ) + WHERE id = $1` + args = []interface{}{accountID} + return dam.DB.QueryRow(query, args...).Err() +} + +type MockDeviceAccountModel struct{} + +func (mdam *MockDeviceAccountModel) Associate(accountID int64, deviceID int64) error { + return nil +} diff --git a/internal/data/devices.go b/internal/data/devices.go new file mode 100644 index 0000000..925958d --- /dev/null +++ b/internal/data/devices.go @@ -0,0 +1,67 @@ +package data + +import ( + "database/sql" + "errors" + "time" +) + +type Device struct { + ID int64 + APNSToken string + Sandbox bool + LastPingedAt int64 +} + +type DeviceModel struct { + DB *sql.DB +} + +func (dm *DeviceModel) Upsert(d *Device) error { + d.LastPingedAt = time.Now().Unix() + + query := ` + INSERT INTO devices (apns_token, sandbox, last_pinged_at) + VALUES ($1, $2, $3) + ON CONFLICT(apns_token) + DO + UPDATE SET last_pinged_at = $3 + RETURNING id` + + args := []interface{}{d.APNSToken, d.Sandbox, d.LastPingedAt} + return dm.DB.QueryRow(query, args...).Scan(&d.ID) +} + +func (dm *DeviceModel) GetByAPNSToken(token string) (*Device, error) { + query := ` + SELECT id, apns_token, sandbox, last_pinged_at + FROM devices + WHERE apns_token = $1` + + device := &Device{} + err := dm.DB.QueryRow(query, token).Scan( + &device.ID, + &device.APNSToken, + &device.Sandbox, + &device.LastPingedAt, + ) + + if err != nil { + switch { + case errors.Is(err, sql.ErrNoRows): + return nil, ErrRecordNotFound + default: + return nil, err + } + } + return device, nil +} + +type MockDeviceModel struct{} + +func (mdm *MockDeviceModel) Upsert(d *Device) error { + return nil +} +func (mdm *MockDeviceModel) GetByAPNSToken(token string) (*Device, error) { + return nil, nil +} diff --git a/internal/data/models.go b/internal/data/models.go new file mode 100644 index 0000000..1e3dd79 --- /dev/null +++ b/internal/data/models.go @@ -0,0 +1,42 @@ +package data + +import ( + "database/sql" + "errors" +) + +var ( + ErrRecordNotFound = errors.New("record not found") +) + +type Models struct { + Accounts interface { + Upsert(a *Account) error + Delete(id int64) error + } + + Devices interface { + Upsert(*Device) error + GetByAPNSToken(string) (*Device, error) + } + + DevicesAccounts interface { + Associate(int64, int64) error + } +} + +func NewModels(db *sql.DB) *Models { + return &Models{ + Accounts: &AccountModel{DB: db}, + Devices: &DeviceModel{DB: db}, + DevicesAccounts: &DeviceAccountModel{DB: db}, + } +} + +func NewMockModels() *Models { + return &Models{ + Accounts: &MockAccountModel{}, + Devices: &MockDeviceModel{}, + DevicesAccounts: &MockDeviceAccountModel{}, + } +} diff --git a/internal/reddit/client.go b/internal/reddit/client.go new file mode 100644 index 0000000..25577cc --- /dev/null +++ b/internal/reddit/client.go @@ -0,0 +1,105 @@ +package reddit + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" +) + +const ( + tokenURL = "https://www.reddit.com/api/v1/access_token" +) + +type Client struct { + id string + secret string +} + +func NewClient(id, secret string) *Client { + return &Client{id, secret} +} + +type AuthenticatedClient struct { + *Client + + refreshToken string + accessToken string + expiry *time.Time +} + +func (rc *Client) NewAuthenticatedClient(refreshToken, accessToken string) *AuthenticatedClient { + return &AuthenticatedClient{rc, refreshToken, accessToken, nil} +} + +func (rac *AuthenticatedClient) request(r *Request) ([]byte, error) { + tr := &http.Transport{} + client := &http.Client{Transport: tr} + + req, err := r.HTTPRequest() + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + return ioutil.ReadAll(resp.Body) +} + +func (rac *AuthenticatedClient) refreshTokens() (string, string, error) { + req := NewRequest( + WithMethod("POST"), + WithURL(tokenURL), + WithBody("grant_type", "refresh_token"), + WithBody("refresh_token", rac.refreshToken), + WithBasicAuth(rac.id, rac.secret), + ) + + body, err := rac.request(req) + fmt.Println(string(body)) + return "", "", err +} + +func (rac *AuthenticatedClient) MessageInbox() error { + req := NewRequest( + WithMethod("GET"), + WithToken(rac.accessToken), + WithURL("https://oauth.reddit.com/message/inbox.json"), + ) + + body, _ := rac.request(req) + fmt.Println(string(body)) + return nil +} + +type MeResponse struct { + Name string +} + +func (mr *MeResponse) NormalizedUsername() string { + return strings.ToLower(mr.Name) +} + +func (rac *AuthenticatedClient) Me() (*MeResponse, error) { + req := NewRequest( + WithMethod("GET"), + WithToken(rac.accessToken), + WithURL("https://oauth.reddit.com/api/v1/me"), + ) + + body, err := rac.request(req) + + if err != nil { + return nil, err + } + + mr := &MeResponse{} + err = json.Unmarshal(body, mr) + + return mr, err +} diff --git a/internal/reddit/request.go b/internal/reddit/request.go new file mode 100644 index 0000000..8c03b0e --- /dev/null +++ b/internal/reddit/request.go @@ -0,0 +1,76 @@ +package reddit + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/url" + "strings" +) + +const userAgent = "server:test-api:v0.0.1 (by /u/changelog)" + +type Request struct { + body url.Values + method string + token string + url string + auth string +} + +type RequestOption func(*Request) + +func NewRequest(opts ...RequestOption) *Request { + req := &Request{url.Values{}, "GET", "", "", ""} + for _, opt := range opts { + opt(req) + } + + return req +} + +func (r *Request) HTTPRequest() (*http.Request, error) { + req, err := http.NewRequest(r.method, r.url, strings.NewReader(r.body.Encode())) + req.Header.Add("User-Agent", userAgent) + + if r.token != "" { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token)) + } + + if r.auth != "" { + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", r.auth)) + } + + return req, err +} + +func WithMethod(method string) RequestOption { + return func(req *Request) { + req.method = method + } +} + +func WithURL(url string) RequestOption { + return func(req *Request) { + req.url = url + } +} + +func WithBasicAuth(user, password string) RequestOption { + return func(req *Request) { + encoded := base64.StdEncoding.EncodeToString([]byte(user + ":" + password)) + req.auth = encoded + } +} + +func WithToken(token string) RequestOption { + return func(req *Request) { + req.token = token + } +} + +func WithBody(key, val string) RequestOption { + return func(req *Request) { + req.body.Set(key, val) + } +} diff --git a/migrations/20210509150615_create_devices.down.sql b/migrations/20210509150615_create_devices.down.sql new file mode 100644 index 0000000..2af6915 --- /dev/null +++ b/migrations/20210509150615_create_devices.down.sql @@ -0,0 +1 @@ +DROP TABLE devices; diff --git a/migrations/20210509150615_create_devices.up.sql b/migrations/20210509150615_create_devices.up.sql new file mode 100644 index 0000000..e78478c --- /dev/null +++ b/migrations/20210509150615_create_devices.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS devices ( + id SERIAL PRIMARY KEY, + apns_token character(100) UNIQUE, + sandbox boolean, + last_pinged_at integer +); +CREATE UNIQUE INDEX IF NOT EXISTS devices_pkey ON devices(id int4_ops); +CREATE UNIQUE INDEX IF NOT EXISTS devices_apns_token_key ON devices(apns_token bpchar_ops); diff --git a/migrations/20210509191431_create_accounts.down.sql b/migrations/20210509191431_create_accounts.down.sql new file mode 100644 index 0000000..5eb8e05 --- /dev/null +++ b/migrations/20210509191431_create_accounts.down.sql @@ -0,0 +1 @@ +DROP TABLE accounts; diff --git a/migrations/20210509191431_create_accounts.up.sql b/migrations/20210509191431_create_accounts.up.sql new file mode 100644 index 0000000..42c6ada --- /dev/null +++ b/migrations/20210509191431_create_accounts.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS accounts ( + id SERIAL PRIMARY KEY, + username character(20), + access_token character(64), + refresh_token character(64), + expires_at integer, + last_message_id character(32), + device_count integer, + last_checked_at integer +); + +CREATE UNIQUE INDEX IF NOT EXISTS accounts_pkey ON accounts(id int4_ops); +CREATE UNIQUE INDEX IF NOT EXISTS accounts_username_key ON accounts(username bpchar_ops); diff --git a/migrations/20210509200230_create_devices_accounts.down.sql b/migrations/20210509200230_create_devices_accounts.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/migrations/20210509200230_create_devices_accounts.up.sql b/migrations/20210509200230_create_devices_accounts.up.sql new file mode 100644 index 0000000..812f6ff --- /dev/null +++ b/migrations/20210509200230_create_devices_accounts.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS devices_accounts ( + id SERIAL PRIMARY KEY, + account_id integer, + device_id integer +); +CREATE UNIQUE INDEX IF NOT EXISTS devices_accounts_pkey ON devices_accounts(id int4_ops); +CREATE UNIQUE INDEX IF NOT EXISTS devices_accounts_account_id_device_id_idx ON devices_accounts(account_id int4_ops,device_id int4_ops); diff --git a/script/bootstrap b/script/bootstrap new file mode 100755 index 0000000..874857f --- /dev/null +++ b/script/bootstrap @@ -0,0 +1,20 @@ +#!/bin/sh + +set -e + +cd "$(dirname "$0")/.." + +brew bundle check >/dev/null 2>&1 || { + echo "==> Installing Homebrew dependencies..." + brew bundle +} + +[ -d "tmp/postgresql" ] || { + echo "===> Setting up database..." + initdb -D tmp/postgresql -U apollo +} + +go mod verify >/dev/null 2>&1 || { + echo "==> Installing Go dependencies..." + go mod download +} diff --git a/script/migrate b/script/migrate new file mode 100755 index 0000000..d7dd14c --- /dev/null +++ b/script/migrate @@ -0,0 +1,16 @@ +#!/bin/sh + +set -e + +cd "$(dirname "$0")/.." + +# ensure everything in the app is up to date. +script/bootstrap + +[ -f ".env" ] && { + source .env + + echo "===> Running migrations..." + createdb -U apollo apollo || true + migrate -path=./migrations -database=$DATABASE_URL up +} diff --git a/script/server b/script/server new file mode 100755 index 0000000..d63a124 --- /dev/null +++ b/script/server @@ -0,0 +1,11 @@ +#!/bin/sh + +set -e + +cd "$(dirname "$0")/.." + +# ensure everything in the app is up to date. +script/bootstrap + +# boot the app and any other necessary processes. +foreman start