diff --git a/Dockerfile b/Dockerfile index 9a8f2ec..1800bc8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,14 @@ # Building the binary of the App FROM golang:1.19 AS build +ARG arch=amd64 WORKDIR /go/src/tasky COPY . . RUN go mod download -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /go/src/tasky/tasky +RUN CGO_ENABLED=0 GOOS=linux GOARCH="$ARG" go build -o /go/src/tasky/tasky -FROM alpine:3.17.0 as release +FROM alpine:3.19.0 as release WORKDIR /app COPY --from=build /go/src/tasky/tasky . diff --git a/README.md b/README.md index 82e1443..31cf670 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ The following environment variables are needed. |Variable|Purpose|example| |---|---|---| |`MONGODB_URI`|Address to mongo server|`mongodb://servername:27017` or `mongodb://username:password@hostname:port` or `mongodb+srv://` schema| +|`POSTGRES_URI`|Address to postgres server (`DB_TYPE` must be defined)|`postgresql://servername:5432` or `postgresql://user:pass@hostname:port`| |`SECRET_KEY`|Secret key for JWT tokens|`secret123`| +|`DB_TYPE`|Specify database type (`mongodb` or `postgres`); default is `mongodb`|`mongodb`| Alternatively, you can create a `.env` file and load it up with the environment variables. @@ -23,4 +25,4 @@ Run the command `go run main.go` and the project should run on `locahost:8080` This project is licensed under the terms of the MIT license. -Original project: https://github.com/dogukanozdemir/golang-todo-mongodb \ No newline at end of file +Original project: https://github.com/dogukanozdemir/golang-todo-mongodb diff --git a/controllers/todoController.go b/controllers/todoController.go index ae01572..57d9a06 100644 --- a/controllers/todoController.go +++ b/controllers/todoController.go @@ -6,25 +6,18 @@ import ( "net/http" "time" + "github.com/gin-gonic/gin" "github.com/jeffthorne/tasky/auth" "github.com/jeffthorne/tasky/database" "github.com/jeffthorne/tasky/models" - "github.com/gin-gonic/gin" - "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" ) -var todoCollection *mongo.Collection = database.OpenCollection(database.Client, "todos") - -func GetTodo(c *gin.Context) { +func GetTodo(c *gin.Context, db database.DBClient) { var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) id := c.Param("id") - objId, _ := primitive.ObjectIDFromHex(id) - - var todo models.Todo - err := todoCollection.FindOne(ctx, bson.M{"_id": objId}).Decode(&todo) + todo, err := db.GetTodo(ctx, id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error ": err.Error()}) } @@ -33,111 +26,83 @@ func GetTodo(c *gin.Context) { c.JSON(http.StatusOK, todo) } -func ClearAll(c *gin.Context) { +func ClearAll(c *gin.Context, db database.DBClient) { session := auth.ValidateSession(c) - if !session{ + if !session { return - } - - var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) - userid := c.Param("userid") - _, err := todoCollection.DeleteMany(ctx, bson.M{"userid": userid}) + } + var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() + err := db.ClearTodos(ctx, c.Param("userid")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - defer cancel() c.JSON(http.StatusOK, gin.H{"success": "All todos deleted."}) } -func GetTodos(c *gin.Context) { +func GetTodos(c *gin.Context, db database.DBClient) { session := auth.ValidateSession(c) - if !session{ + if !session { return - } + } var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) - userid := c.Param("userid") - findResult, err := todoCollection.Find(ctx, bson.M{"userid": userid}) + defer cancel() + todos, err := db.GetTodos(ctx, c.Param("userid")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"FindError": err.Error()}) return } - - var todos []models.Todo - for findResult.Next(ctx) { - var todo models.Todo - err := findResult.Decode(&todo) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"Decode Error": err.Error()}) - return - } - todos = append(todos, todo) - } - defer cancel() - c.JSON(http.StatusOK, todos) } -func DeleteTodo(c *gin.Context) { +func DeleteTodo(c *gin.Context, db database.DBClient) { session := auth.ValidateSession(c) - if !session{ + if !session { return - } + } var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) - - id := c.Param("id") - userid := c.Param("userid") - objId, _ := primitive.ObjectIDFromHex(id) - deleteResult, err := todoCollection.DeleteOne(ctx, bson.M{"_id": objId, "userid": userid}) + defer cancel() + msg, err := db.DeleteTodo(ctx, c.Param("id"), c.Param("userid")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - if deleteResult.DeletedCount == 0 { - msg := fmt.Sprintf("No todo with id : %v was found, no deletion occurred.", id) - c.JSON(http.StatusBadRequest, gin.H{"error": msg}) - return - } - defer cancel() - - msg := fmt.Sprintf("todo with id : %v was deleted successfully.", id) c.JSON(http.StatusOK, gin.H{"success": msg}) } -func UpdateTodo(c *gin.Context) { +func UpdateTodo(c *gin.Context, db database.DBClient) { session := auth.ValidateSession(c) - if !session{ + if !session { return - } + } var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() var newTodo models.Todo if err := c.BindJSON(&newTodo); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - - _, err := todoCollection.UpdateOne(ctx, bson.M{"_id": newTodo.ID, "userid" : newTodo.UserID}, bson.M{"$set": newTodo}) - if err != nil { + if err := db.UpdateTodo(ctx, &newTodo); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) fmt.Println(err.Error()) return } - defer cancel() - c.JSON(http.StatusOK, newTodo) } -func AddTodo(c *gin.Context) { +func AddTodo(c *gin.Context, db database.DBClient) { session := auth.ValidateSession(c) - if !session{ + if !session { return - } + } var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() var todo models.Todo if err := c.BindJSON(&todo); err != nil { @@ -148,11 +113,10 @@ func AddTodo(c *gin.Context) { todo.ID = primitive.NewObjectID() todo.UserID = c.Param("userid") - _, err := todoCollection.InsertOne(ctx, todo) + err := db.AddTodo(ctx, &todo) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - defer cancel() c.JSON(http.StatusOK, gin.H{"insertedId": todo.ID}) } diff --git a/controllers/userController.go b/controllers/userController.go index c1807a2..24e5ba6 100644 --- a/controllers/userController.go +++ b/controllers/userController.go @@ -8,30 +8,25 @@ import ( "os" "time" + "github.com/gin-gonic/gin" "github.com/jeffthorne/tasky/auth" "github.com/jeffthorne/tasky/database" "github.com/jeffthorne/tasky/models" - "github.com/gin-gonic/gin" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" "golang.org/x/crypto/bcrypt" ) var SECRET_KEY string = os.Getenv("SECRET_KEY") -var userCollection *mongo.Collection = database.OpenCollection(database.Client, "user") - -func SignUp(c * gin.Context){ - +func SignUp(c *gin.Context, db database.DBClient) { var user models.User if err := c.BindJSON(&user); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() - emailCount, err := userCollection.CountDocuments(ctx, bson.M{"email": user.Email}) + emailCount, err := db.FindExistingUsers(ctx, user) defer cancel() if err != nil { @@ -46,14 +41,7 @@ func SignUp(c * gin.Context){ c.JSON(http.StatusBadRequest, gin.H{"error": "User with this email already exists!"}) return } - user.ID = primitive.NewObjectID() - resultInsertionNumber, insertErr := userCollection.InsertOne(ctx, user) - if insertErr != nil { - msg := fmt.Sprintf("user item was not created") - c.JSON(http.StatusInternalServerError, gin.H{"error": msg}) - return - } - defer cancel() + msg, err := db.AddUser(ctx, &user) userId := user.ID.Hex() username := *user.Name @@ -70,33 +58,30 @@ func SignUp(c * gin.Context){ }) http.SetCookie(c.Writer, &http.Cookie{ - Name : "userID", - Value : userId, + Name: "userID", + Value: userId, Expires: expirationTime, }) http.SetCookie(c.Writer, &http.Cookie{ - Name : "username", - Value : username, + Name: "username", + Value: username, Expires: expirationTime, }) - c.JSON(http.StatusOK, resultInsertionNumber) - + c.JSON(http.StatusOK, msg) } -func Login(c * gin.Context){ +func Login(c *gin.Context, db database.DBClient) { var user models.User - var foundUser models.User - + if err := c.BindJSON(&user); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "bind error"}) return } var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) - - err := userCollection.FindOne(ctx, bson.M{"email": user.Email}).Decode(&foundUser) defer cancel() + foundUser, err := db.GetUser(ctx, *user.Email) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": " email or password is incorrect"}) return @@ -116,14 +101,14 @@ func Login(c * gin.Context){ } userId := foundUser.ID.Hex() username := *foundUser.Name - + shouldRefresh, err, expirationTime := auth.RefreshToken(c) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "refresh token error"}) return } - if shouldRefresh{ + if shouldRefresh { token, err, expirationTime := auth.GenerateJWT(userId) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "error occured while generating token"}) @@ -137,35 +122,35 @@ func Login(c * gin.Context){ }) http.SetCookie(c.Writer, &http.Cookie{ - Name : "userID", - Value : userId, + Name: "userID", + Value: userId, Expires: expirationTime, }) http.SetCookie(c.Writer, &http.Cookie{ - Name : "username", - Value : username, + Name: "username", + Value: username, Expires: expirationTime, }) - + } else { http.SetCookie(c.Writer, &http.Cookie{ - Name : "userID", - Value : userId, + Name: "userID", + Value: userId, Expires: expirationTime, }) http.SetCookie(c.Writer, &http.Cookie{ - Name : "username", - Value : username, + Name: "username", + Value: username, Expires: expirationTime, }) } c.JSON(http.StatusOK, gin.H{"msg": "login successful"}) } -func Todo(c * gin.Context) { +func Todo(c *gin.Context, db database.DBClient) { session := auth.ValidateSession(c) if session { - c.HTML(http.StatusOK,"todo.html", nil) + c.HTML(http.StatusOK, "todo.html", nil) } } @@ -188,4 +173,4 @@ func VerifyPassword(userPassword string, providedPassword string) (bool, string) } return check, msg -} \ No newline at end of file +} diff --git a/database/database.go b/database/database.go index 5f69561..736ec06 100644 --- a/database/database.go +++ b/database/database.go @@ -3,35 +3,38 @@ package database import ( "context" "fmt" - "log" "os" - "time" + "strings" + "github.com/jeffthorne/tasky/models" "github.com/joho/godotenv" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" ) -var Client *mongo.Client = CreateMongoClient() +type DBClient interface { + FindExistingUsers(ctx context.Context, user models.User) (int64, error) + AddUser(ctx context.Context, user *models.User) ([]byte, error) + GetUser(ctx context.Context, email string) (models.User, error) + GetTodo(ctx context.Context, id string) (models.Todo, error) + GetTodos(ctx context.Context, id string) ([]models.Todo, error) + ClearTodos(ctx context.Context, userid string) error + DeleteTodo(ctx context.Context, id string, userid string) (string, error) + UpdateTodo(ctx context.Context, newTodo *models.Todo) error + AddTodo(ctx context.Context, todo *models.Todo) error + Close() error +} -func CreateMongoClient() *mongo.Client { +func CreateDBClientFromEnv() DBClient { godotenv.Overload() - MongoDbURI := os.Getenv("MONGODB_URI") - client, err := mongo.NewClient(options.Client().ApplyURI(MongoDbURI)) - if err != nil { - log.Fatal(err) + dbType := strings.ToLower(os.Getenv("DB_TYPE")) + if len(dbType) == 0 { + dbType = "mongodb" } - - var ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) - err = client.Connect(ctx) - if err != nil { - log.Fatal(err) + switch dbType { + case "mongodb": + return NewMongoDBClient() + case "postgresql": + return NewPostgresDBClient() + default: + panic(fmt.Sprintf("This database type is unsupported: %s", dbType)) } - defer cancel() - fmt.Println("Connected to MONGO -> ", MongoDbURI) - return client -} - -func OpenCollection(client *mongo.Client, collectionName string) *mongo.Collection { - return client.Database("go-mongodb").Collection(collectionName) } diff --git a/database/mongodb.go b/database/mongodb.go new file mode 100644 index 0000000..2ff5d75 --- /dev/null +++ b/database/mongodb.go @@ -0,0 +1,152 @@ +package database + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "time" + + "github.com/jeffthorne/tasky/models" + "github.com/joho/godotenv" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type MongoDBClient struct { + client *mongo.Client + userCollection *mongo.Collection + todoCollection *mongo.Collection +} + +func (m *MongoDBClient) FindExistingUsers(ctx context.Context, user models.User) (int64, error) { + return m.userCollection.CountDocuments(ctx, bson.M{"email": user.Email}) +} + +func (m *MongoDBClient) AddUser(ctx context.Context, user *models.User) ([]byte, error) { + user.ID = primitive.NewObjectID() + resultInsertionNumber, insertErr := m.userCollection.InsertOne(ctx, user) + if insertErr != nil { + return []byte{}, fmt.Errorf("user item was not created") + } + result, err := json.Marshal(&resultInsertionNumber) + if err != nil { + return []byte{}, err + } + user.ID = primitive.NewObjectID() + return result, nil +} + +func (m *MongoDBClient) GetUser(ctx context.Context, email string) (models.User, error) { + var found models.User + if err := m.userCollection.FindOne(ctx, bson.M{"email": email}).Decode(&found); err != nil { + return found, err + } + return found, nil +} + +func (m *MongoDBClient) GetTodo(ctx context.Context, id string) (models.Todo, error) { + var todo models.Todo + objId, _ := primitive.ObjectIDFromHex(id) + if err := m.todoCollection.FindOne(ctx, bson.M{"_id": objId}).Decode(&todo); err != nil { + return todo, err + } + return todo, nil +} + +func (m *MongoDBClient) GetTodos(ctx context.Context, userid string) ([]models.Todo, error) { + var todos []models.Todo + findResult, err := m.todoCollection.Find(ctx, bson.M{"userid": userid}) + if err != nil { + return todos, err + } + for findResult.Next(ctx) { + var todo models.Todo + err := findResult.Decode(&todo) + if err != nil { + return todos, err + } + todos = append(todos, todo) + } + return todos, nil +} + +func (m *MongoDBClient) ClearTodos(ctx context.Context, userid string) error { + _, err := m.todoCollection.DeleteMany(ctx, bson.M{"userid": userid}) + if err != nil { + return err + } + return nil +} + +func (m *MongoDBClient) DeleteTodo(ctx context.Context, id string, userid string) (string, error) { + objId, _ := primitive.ObjectIDFromHex(id) + deleteResult, err := m.todoCollection.DeleteOne(ctx, bson.M{"_id": objId, "userid": userid}) + if err != nil { + return "", err + } + if deleteResult.DeletedCount == 0 { + return "", fmt.Errorf("No todo with id : %v was found, no deletion occurred.", id) + } + return fmt.Sprintf("todo with id: %v was deleted successfully.", id), nil +} + +func (m *MongoDBClient) UpdateTodo(ctx context.Context, newTodo *models.Todo) error { + _, err := m.todoCollection.UpdateOne(ctx, bson.M{"_id": newTodo.ID, "userid": newTodo.UserID}, bson.M{"$set": newTodo}) + if err != nil { + return err + } + return nil +} + +func (m *MongoDBClient) AddTodo(ctx context.Context, todo *models.Todo) (err error) { + todo.ID = primitive.NewObjectID() + _, err = m.todoCollection.InsertOne(ctx, todo) + if err != nil { + return err + } + return nil +} + +func (m *MongoDBClient) Close() error { + return nil +} + +func NewMongoDBClient() *MongoDBClient { + m := MongoDBClient{} + m.client = createMongoClient() + m.todoCollection = openCollection(m.client, "todos") + m.userCollection = openCollection(m.client, "users") + return &m +} + +func createMongoClient() *mongo.Client { + godotenv.Overload() + MongoDbURI := os.Getenv("MONGODB_URI") + client, err := mongo.NewClient(options.Client().ApplyURI(MongoDbURI)) + if err != nil { + log.Fatal(err) + panic(err) + } + + var ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) + err = client.Connect(ctx) + if err != nil { + log.Fatal(err) + panic(err) + } + defer cancel() + fmt.Println("Connected to MONGO -> ", MongoDbURI) + return client +} + +func openCollection(client *mongo.Client, collectionName string) *mongo.Collection { + MongoDbDatabaseName := os.Getenv("MONGODB_DB") + if len(MongoDbDatabaseName) == 0 { + MongoDbDatabaseName = "go-mongodb" + } + return client.Database(MongoDbDatabaseName).Collection(collectionName) +} diff --git a/database/postgresql.go b/database/postgresql.go new file mode 100644 index 0000000..5d30a48 --- /dev/null +++ b/database/postgresql.go @@ -0,0 +1,146 @@ +package database + +import ( + "context" + "fmt" + "os" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/jeffthorne/tasky/models" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type PostgresDBClient struct { + conn *pgxpool.Pool + ctx context.Context +} + +func (p *PostgresDBClient) FindExistingUsers(ctx context.Context, user models.User) (int64, error) { + rows, err := p.conn.Query(ctx, "SELECT * FROM \"users\" WHERE email = $1", user.Email) + if err != nil { + return 0, err + } + defer rows.Close() + var rowCount int64 + for rows.Next() { + rowCount++ + } + return rowCount, nil +} + +func (p *PostgresDBClient) AddUser(ctx context.Context, user *models.User) ([]byte, error) { + if _, err := p.conn.Query(ctx, "SELECT id FROM \"users\""); err != nil { + return nil, err + } + + user.ID = primitive.NewObjectID() + _, err := p.conn.Exec(ctx, "INSERT INTO users (id, email, password) VALUES ($1, $2, $3)", user.ID, user.Email, user.Password) + if err != nil { + return []byte{}, err + } + return []byte{}, nil +} + +func (p *PostgresDBClient) GetUser(ctx context.Context, email string) (models.User, error) { + var found models.User + if err := p.conn.QueryRow(ctx, "SELECT * FROM \"users\" WHERE email = $1", email).Scan(&found); err != nil { + return found, err + } + return found, nil +} + +func (p *PostgresDBClient) GetTodo(ctx context.Context, id string) (models.Todo, error) { + var todo models.Todo + if err := p.conn.QueryRow(ctx, "SELECT * FROM todos WHERE id = $1", id).Scan(&todo); err != nil { + return todo, err + } + return todo, nil +} + +func (p *PostgresDBClient) GetTodos(ctx context.Context, userid string) ([]models.Todo, error) { + var todos []models.Todo + rows, err := p.conn.Query(ctx, "SELECT * FROM todos WHERE userid = $1", userid) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var todo models.Todo + if err := rows.Scan(&todo); err != nil { + return nil, err + } + todos = append(todos, todo) + } + return todos, nil +} + +func (p *PostgresDBClient) ClearTodos(ctx context.Context, userid string) error { + if _, err := p.conn.Exec(ctx, "DELETE FROM todos WHERE userid = $1", userid); err != nil { + return err + } + return nil +} + +func (p *PostgresDBClient) DeleteTodo(ctx context.Context, id string, userid string) (string, error) { + if _, err := p.conn.Exec(ctx, "DELETE FROM todos WHERE userid = $1 AND id = $2", userid, id); err != nil { + return "", err + } + return "", nil +} + +func (p *PostgresDBClient) UpdateTodo(ctx context.Context, newTodo *models.Todo) error { + if _, err := p.conn.Exec(ctx, "UPDATE todos SET name = $1, status = $2 WHERE userid = $3 AND id = $4", newTodo.Name, newTodo.Status, newTodo.UserID, newTodo.ID); err != nil { + return err + } + return nil +} + +func (p *PostgresDBClient) AddTodo(ctx context.Context, todo *models.Todo) (err error) { + todo.ID = primitive.NewObjectID() + if err != nil { + return err + } + _, err = p.conn.Exec(ctx, "INSERT INTO todos (id, name, status, userid) VALUES ($1, $2, $3, $4)", todo.ID, todo.Name, todo.Status, todo.UserID) + if err != nil { + return err + } + return nil +} + +func (p *PostgresDBClient) Close() error { + p.conn.Close() + return nil +} + +func NewPostgresDBClient() *PostgresDBClient { + p := PostgresDBClient{ctx: context.Background()} + var err error + p.ctx = context.Background() + p.conn, err = pgxpool.New(p.ctx, os.Getenv("POSTGRES_URI")) + if err != nil { + panic(err) + } + if err = createTableIfNotExist(&p, "users", "id text, email text, password text"); err != nil { + panic(err) + } + if err := createTableIfNotExist(&p, "todos", "id text, name text, status text, userid text"); err != nil { + panic(err) + } + return &p +} + +func createTableIfNotExist(p *PostgresDBClient, tableName string, cols string) error { + var exists bool + if err := p.conn.QueryRow(p.ctx, "SELECT EXISTS (SELECT FROM pg_tables WHERE tablename = $1)", tableName).Scan(&exists); err != nil { + return err + } + if exists { + return nil + } + query := fmt.Sprintf("CREATE TABLE %s (%s)", pgx.Identifier.Sanitize([]string{tableName}), cols) + if _, err := p.conn.Exec(p.ctx, query); err != nil { + return err + } + return nil +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..8532dc0 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,57 @@ +volumes: + mongodb-data: {} + postgres-data: {} +services: + app-mongodb: + depends_on: + - mongodb + build: + context: . + ports: + - 8080:8080 + environment: + MONGODB_URI: mongodb://user:supersecret@mongodb:27017/app-db + SECRET_KEY: supersecret + MONGODB_DB: app-db + app-postgres: + depends_on: + - postgres + build: + context: . + ports: + - 8080:8080 + environment: + POSTGRES_URI: postgresql://user:supersecret@postgres:5432/app-db + SECRET_KEY: supersecret + MONGODB_DB: app-db + DB_TYPE: postgresql + mongodb: + build: + dockerfile: mongo.Dockerfile + context: . + environment: + MONGODB_USER: user + MONGODB_PASS: supersecret + MONGODB_DB: app-db + volumes: + - mongodb-data:/data/db + healthcheck: + test: [ "CMD", "nc", "-z", "localhost", "27017" ] + interval: 5s + timeout: 20s + retries: 10 + start_period: 3s + postgres: + image: postgres:16.2-alpine + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: supersecret + POSTGRES_DB: app-db + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: [ "CMD", "pg_isready", "-d", "app-db", "-U", "user" ] + interval: 5s + timeout: 20s + retries: 10 + start_period: 3s diff --git a/go.mod b/go.mod index 2d6a4cb..eb54b6a 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,10 @@ go 1.18 require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gin-gonic/gin v1.8.1 + github.com/jackc/pgx/v5 v5.5.5 + github.com/joho/godotenv v1.4.0 go.mongodb.org/mongo-driver v1.9.1 - golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 + golang.org/x/crypto v0.17.0 ) require ( @@ -17,7 +19,9 @@ require ( github.com/go-stack/stack v1.8.0 // indirect github.com/goccy/go-json v0.9.7 // indirect github.com/golang/snappy v0.0.1 // indirect - github.com/joho/godotenv v1.4.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/leodido/go-urn v1.2.1 // indirect @@ -31,10 +35,10 @@ require ( github.com/xdg-go/scram v1.0.2 // indirect github.com/xdg-go/stringprep v1.0.2 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect - golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect - golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect - golang.org/x/text v0.3.6 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index a7da25f..042660c 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,14 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -64,8 +72,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= @@ -83,29 +91,34 @@ go.mongodb.org/mongo-driver v1.9.1 h1:m078y9v7sBItkt1aaoe2YlvWEXcD263e1a4E1fBrJ1 go.mongodb.org/mongo-driver v1.9.1/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -121,5 +134,5 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go index 8c8e21d..7749f41 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,10 @@ package main import ( "net/http" - controller "github.com/jeffthorne/tasky/controllers" + "github.com/gin-gonic/gin" + controller "github.com/jeffthorne/tasky/controllers" + "github.com/jeffthorne/tasky/database" "github.com/joho/godotenv" ) @@ -11,26 +13,69 @@ func index(c *gin.Context) { c.HTML(http.StatusOK, "login.html", nil) } +type ControllerMap struct { + db database.DBClient +} + +func (m *ControllerMap) GetTodos(c *gin.Context) { + controller.GetTodos(c, m.db) +} + +func (m *ControllerMap) GetTodo(c *gin.Context) { + controller.GetTodo(c, m.db) +} + +func (m *ControllerMap) AddTodo(c *gin.Context) { + controller.AddTodo(c, m.db) +} + +func (m *ControllerMap) DeleteTodo(c *gin.Context) { + controller.DeleteTodo(c, m.db) +} + +func (m *ControllerMap) SignUp(c *gin.Context) { + controller.SignUp(c, m.db) +} + +func (m *ControllerMap) Login(c *gin.Context) { + controller.Login(c, m.db) +} + +func (m *ControllerMap) ClearAll(c *gin.Context) { + controller.ClearAll(c, m.db) +} + +func (m *ControllerMap) UpdateTodo(c *gin.Context) { + controller.UpdateTodo(c, m.db) +} + +func (m *ControllerMap) Todo(c *gin.Context) { + controller.Todo(c, m.db) +} + func main() { godotenv.Overload() - + + db := database.CreateDBClientFromEnv() + defer db.Close() + m := ControllerMap{db: db} + router := gin.Default() router.LoadHTMLGlob("assets/*.html") router.Static("/assets", "./assets") router.GET("/", index) - router.GET("/todos/:userid", controller.GetTodos) - router.GET("/todo/:id", controller.GetTodo) - router.POST("/todo/:userid", controller.AddTodo) - router.DELETE("/todo/:userid/:id", controller.DeleteTodo) - router.DELETE("/todos/:userid", controller.ClearAll) - router.PUT("/todo", controller.UpdateTodo) - + router.GET("/todos/:userid", m.GetTodos) + router.GET("/todo/:id", m.GetTodo) + router.POST("/todo/:userid", m.AddTodo) + router.DELETE("/todo/:userid/:id", m.DeleteTodo) + router.DELETE("/todos/:userid", m.ClearAll) + router.PUT("/todo", m.UpdateTodo) - router.POST("/signup", controller.SignUp) - router.POST("/login", controller.Login) - router.GET("/todo", controller.Todo) + router.POST("/signup", m.SignUp) + router.POST("/login", m.Login) + router.GET("/todo", m.Todo) - router.Run(":8080" ) + router.Run(":8080") } diff --git a/models/models.go b/models/models.go index 0d84173..ae9e7b2 100644 --- a/models/models.go +++ b/models/models.go @@ -12,9 +12,8 @@ type Todo struct { } type User struct { - ID primitive.ObjectID `bson:"_id"` - Name *string `json:"username" bson:"username"` - Email *string `json:"email" bson:"email"` - Password *string `json:"password" bson:"password"` + ID primitive.ObjectID `bson:"_id"` + Name *string `json:"username" bson:"username"` + Email *string `json:"email" bson:"email"` + Password *string `json:"password" bson:"password"` } - diff --git a/mongo.Dockerfile b/mongo.Dockerfile new file mode 100644 index 0000000..69ecce2 --- /dev/null +++ b/mongo.Dockerfile @@ -0,0 +1,14 @@ +FROM alpine:3.19 +ENV MONGOMS_SYSTEM_BINARY=/usr/bin/mongod + +# Have to revert to Alpine 3.9 repo caches to get MongoDB +RUN echo "https://dl-cdn.alpinelinux.org/alpine/v3.9/main" >> /etc/apk/repositories +RUN echo "https://dl-cdn.alpinelinux.org/alpine/v3.9/community" >> /etc/apk/repositories +RUN cat /etc/apk/repositories + +RUN apk add --update mongodb yaml-cpp=0.6.2-r2 +RUN apk add bash netcat-openbsd +COPY ./scripts/mongodb_entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/scripts/mongodb_entrypoint.sh b/scripts/mongodb_entrypoint.sh new file mode 100644 index 0000000..978118f --- /dev/null +++ b/scripts/mongodb_entrypoint.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +MONGODB_USER="${MONGODB_USER?Please define MONGODB_USER}" +MONGODB_PASS="${MONGODB_PASS?Please define MONGODB_PASS}" +MONGODB_DB="${MONGODB_DB?Please define MONGODB_DB}" + +# Create an ephemeral database directory if not mounted externally. +[ ! -d /data/db ] && mkdir -p /data/db + +# Start `mongod` unauth'ed. +mongod --fork --logpath /tmp/mongo.log + +# Wait for mongo to start +attempts=0 +while [ "$attempts" -lt 60 ] +do + nc -w 1 -z localhost 27017 && break + attempts=$((attempts+1)) + >&2 echo "INFO: [${attempts}/60] waiting for mongodb to start..." + sleep 0.25 +done +# Create the MongoDB user in the MONGODB_DB database. +if ! mongo --quiet --eval 'db.getUsers()' "$MONGODB_DB" +then mongo --eval 'db.createUser({user: "'"$MONGODB_USER"'", pwd: "'"$MONGODB_PASS"'", roles: [{ role: "dbOwner", db: "'"$MONGODB_DB"'" }]})' "$MONGODB_DB" +fi + +# Restart `mongod` with auth and logs forwarded to stdout +killall mongod +# Wait for mongo to stop +attempts=0 +while [ "$attempts" -lt 60 ] +do + nc -w 1 -z localhost 27017 || break + attempts=$((attempts+1)) + >&2 echo "INFO: [${attempts}/60] waiting for mongodb to stop..." + sleep 0.25 +done +mongod --auth --bind_ip_all