diff --git a/ui/.air.toml b/ui/.air.toml index 7e46587..09d1607 100644 --- a/ui/.air.toml +++ b/ui/.air.toml @@ -5,9 +5,9 @@ tmp_dir = "tmp" [build] args_bin = [] bin = "./tmp/main" - cmd = "rm **/*_templ.go && templ generate && go build -o ./tmp/main ." + cmd = "rm **/*_templ.go; templ generate --path ./templates && go build -o ./tmp/main ." delay = 1000 - exclude_dir = ["assets", "tmp", "vendor", "testdata", "dist"] + exclude_dir = ["assets", "tmp", "vendor", "testdata", "dist", "docker"] exclude_file = [] exclude_regex = ["_test.go", "_templ.go"] exclude_unchanged = false diff --git a/ui/.gitignore b/ui/.gitignore index c0cdef1..6a93c70 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -1,3 +1,4 @@ dist/* tmp/* **/*_templ.go +docker/tmp/* diff --git a/ui/config/config.go b/ui/config/config.go index 261c471..9354ee1 100644 --- a/ui/config/config.go +++ b/ui/config/config.go @@ -5,26 +5,23 @@ import ( ) type config struct { - Mongo *MongoConfig `json:"mogno"` - JwtSecret string `json:"jwt_secret"` - Env string `json:"env"` + Mongo *MongoConfig `mapstructure:"mongo"` + JwtSecret string `mapstructure:"jwt_secret"` + Env string `mapstructure:"env"` } type MongoConfig struct { - Host string `json:"host"` - Port string `json:"port"` - User string `json:"user"` - Password string `json:"password"` - EntDb string `json:"entity_db"` - EntCol string `json:"entity_col"` + Uri string `mapstructure:"uri"` + EntDb string `mapstructure:"ent_db"` + EntCol string `mapstructure:"ent_col"` } var cfg *config -func init() { +func Init() { viper.SetConfigName("config") // name of config file (without extension) viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name - viper.AddConfigPath("/etc/capstone/") // path to look for the config file in + viper.AddConfigPath("/etc/capstone") // path to look for the config file in err := viper.ReadInConfig() if err != nil { diff --git a/ui/controllers/auth.go b/ui/controllers/auth.go new file mode 100644 index 0000000..bd0afdf --- /dev/null +++ b/ui/controllers/auth.go @@ -0,0 +1,160 @@ +package controllers + +import ( + "fmt" + "time" + + "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config" + "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models" + "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/templates" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +type LoginPostBody struct { + Email string `json:"email"` + Password string `json:"password"` +} + +func SignUpHandler (c *gin.Context) { + //get uname and password. + conf := config.Config() + reqBody := &LoginPostBody{} + c.Request.ParseForm() + reqBody.Email = c.Request.FormValue("email") + reqBody.Password = c.Request.FormValue("password") + + if reqBody.Email == "" { + renderTempl(c, templates.SignupPage("Please provide an email")) + return + } + + if reqBody.Password == "" { + renderTempl(c, templates.SignupPage("Please provide a password")) + return + } + + //Verify username and password + user, err := mongo.FindUserByEmail(reqBody.Email) + if err != nil { + renderTempl(c, templates.SignupPage("Error occured. Please try again later")) + return + } + + if user != nil { + renderTempl(c, templates.SignupPage(fmt.Sprintf("user already exists for %s", reqBody.Email))) + return + } + + user = &models.User{} + + passHash, err := bcrypt.GenerateFromPassword([]byte(reqBody.Password), 10) + if err != nil { + renderTempl(c, templates.SignupPage("Signup failed. Please try again later")) + return + } + + user.PassowrdHash = string(passHash) + user.Email = reqBody.Email + + err = mongo.SaveModel(user) + if err != nil { + renderTempl(c, templates.SignupPage("Signup failed. Please try again later")) + return + } + + //build jwt + token := jwt.NewWithClaims(jwt.SigningMethodHS256, + jwt.MapClaims{ + "sub": user.UserId, + "exp": time.Now().Add(12 * time.Hour).Unix(), + }, + ) + + jwtStr, err := token.SignedString(conf.JwtSecret) + if err != nil { + renderTempl(c, templates.SignupPage("Signup failed. Please try again later")) + return + } + + //store jwt as cookie + //TODO: Make sure set secure for prd deployment + c.SetCookie("authorization", jwtStr, 3600 * 24, "", "", false, true) + + c.Redirect(302, "/dashboard") +} + +func LoginHandler(c *gin.Context) { + //get uname and password. + conf := config.Config() + reqBody := &LoginPostBody{} + c.Request.ParseForm() + reqBody.Email = c.Request.FormValue("email") + reqBody.Password = c.Request.FormValue("password") + + if reqBody.Email == "" { + renderTempl(c, templates.LoginPage("Please provide an email")) + return + } + + if reqBody.Password == "" { + renderTempl(c, templates.LoginPage("Please provide a password")) + return + } + + //Verify username and password + user, err := mongo.FindUserByEmail(reqBody.Email) + if err != nil { + renderTempl(c, templates.LoginPage(err.Error())) + return + } + + if user == nil { + renderTempl(c, templates.LoginPage(fmt.Sprintf("No user found for %s", reqBody.Email))) + return + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.PassowrdHash), []byte(reqBody.Password)); err != nil { + renderTempl(c, templates.LoginPage("Email and password are incorrect")) + return + } + + //build jwt + token := jwt.NewWithClaims(jwt.SigningMethodHS256, + jwt.MapClaims{ + "sub": user.UserId, + "exp": time.Now().Add(12 * time.Hour).Unix(), + }, + ) + + jwtStr, err := token.SignedString(conf.JwtSecret) + if err != nil { + renderTempl(c, templates.LoginPage("An error occured. Please try again later")) + } + + //store jwt as cookie + var secure bool + if conf.Env == "dev" { + secure = false + } else { + secure = true + } + c.SetCookie("authorization", jwtStr, 3600 * 24, "", "", secure, true) + + c.Redirect(302, "/dashboard") +} + +func LogoutHandler(c *gin.Context) { + conf := config.Config() + + var secure bool + if conf.Env == "dev" { + secure = false + } else { + secure = true + } + c.SetCookie("authorization", "", 3600 * 24, "", "", secure, true) + + c.Redirect(302, "/login") +} diff --git a/ui/controllers/controllers.go b/ui/controllers/controllers.go index 20c9900..18a24a6 100644 --- a/ui/controllers/controllers.go +++ b/ui/controllers/controllers.go @@ -1,21 +1,37 @@ package controllers import ( + "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config" + "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db" "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/middleware" - "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/templates" "github.com/gin-gonic/gin" ) +var mongo *db.DB +var log *logrus.Logger func BuildRouter(r *gin.Engine) { + conf := config.Config() + + var err error + mongo, err = db.NewClient(conf.Mongo.Uri) + if err != nil { + panic(err) + } + r.GET("/", middleware.AuthMiddleware(false) ,LandingPage) r.GET("/login", middleware.AuthMiddleware(false), LoginPage) + r.GET("/signup", middleware.AuthMiddleware(false), SignUpPage) + + r.POST("/login", LoginHandler) + r.POST("/signup", SignUpHandler) + r.POST("/logout", LogoutHandler) + + dashboard := r.Group("/dashboard") + dashboard.Use(middleware.AuthMiddleware(true)) + dashboard.GET("/", DashboardPage) } -func LandingPage(c *gin.Context) { - renderTempl(c, templates.LandingPage(false)) -} -func LoginPage(c *gin.Context) { - renderTempl(c, templates.LoginPage()) -} + + diff --git a/ui/controllers/pages.go b/ui/controllers/pages.go new file mode 100644 index 0000000..62f47b8 --- /dev/null +++ b/ui/controllers/pages.go @@ -0,0 +1,22 @@ +package controllers + +import ( + "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/templates" + "github.com/gin-gonic/gin" +) + +func LandingPage(c *gin.Context) { + renderTempl(c, templates.LandingPage(false)) +} + +func LoginPage(c *gin.Context) { + renderTempl(c, templates.LoginPage("")) +} + +func SignUpPage(c *gin.Context) { + renderTempl(c, templates.SignupPage("")) +} + +func DashboardPage(c *gin.Context) { + c.JSON(200, gin.H{"response": "dashboard"}) +} diff --git a/ui/controllers/util.go b/ui/controllers/util.go index 06b9402..4392eec 100644 --- a/ui/controllers/util.go +++ b/ui/controllers/util.go @@ -14,3 +14,20 @@ func renderTempl(c *gin.Context, tmpl templ.Component) { c.Data(200, "text/html", buf.Bytes()) } + +func badRequest(c *gin.Context, reason string) { + c.JSON(400, map[string]string{"error": reason}) +} + + +func serverError(c *gin.Context, reason string) { + c.JSON(504, map[string]string{"error": reason}) +} + +func notFound(c *gin.Context, reason string) { + c.JSON(404, map[string]string{"error": reason}) +} + +func unauthorized(c *gin.Context, reason string) { + c.JSON(403, map[string]string{"error": reason}) +} diff --git a/ui/db/db.go b/ui/db/db.go index 97260c5..eb5ab00 100644 --- a/ui/db/db.go +++ b/ui/db/db.go @@ -24,3 +24,7 @@ func NewClient(uri string) (*DB, error) { return &DB{client: client}, nil } + +func (db *DB) SaveModel(m Model) error { + return m.Save(db.client) +} diff --git a/ui/db/models/user.go b/ui/db/models/user.go index c8c8b9b..d56cc32 100644 --- a/ui/db/models/user.go +++ b/ui/db/models/user.go @@ -28,9 +28,11 @@ func (user *User) Save(client *mongo.Client) error { if user.mongoId.IsZero() { now := time.Now() - user.EntityType = USER_TYPE - user.CreatedAt = now - user.UpdatedAt = now + user.model = &model{ + EntityType: USER_TYPE, + CreatedAt: now, + UpdatedAt: now, + } user.UserId = uuid.New().String() user.mongoId = primitive.NewObjectIDFromTimestamp(now) } diff --git a/ui/db/user.go b/ui/db/user.go index 74dd131..077d3e9 100644 --- a/ui/db/user.go +++ b/ui/db/user.go @@ -6,6 +6,7 @@ import ( "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config" "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) @@ -16,6 +17,9 @@ func (db *DB) FindUserByEmail(email string) (*models.User, error) { res := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).FindOne(context.Background(), bson.M{"email": email}, opts) if res.Err() != nil { + if res.Err() == mongo.ErrNoDocuments { + return nil, nil + } return nil, res.Err() } @@ -35,6 +39,9 @@ func (db *DB) FindUserById(id string) (*models.User, error) { res := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).FindOne(context.Background(), bson.M{"user_id": id}, opts) if res.Err() != nil { + if res.Err() == mongo.ErrNoDocuments { + return nil, nil + } return nil, res.Err() } @@ -54,6 +61,9 @@ func (db *DB) FindAllUsers() ([]models.User, error) { res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Find(context.Background(), bson.M{"ent": models.USER_TYPE}, opts) if err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } return nil, res.Err() } diff --git a/ui/docker/docker-compose.yaml b/ui/docker/docker-compose.yaml new file mode 100644 index 0000000..466fbef --- /dev/null +++ b/ui/docker/docker-compose.yaml @@ -0,0 +1,8 @@ +version: '3.8' +services: + mongodb: + image: mongo:7 + ports: + - '27017:27017' + volumes: + - ./tmp/mongo:/data/db diff --git a/ui/go.mod b/ui/go.mod index 2c08a24..f325e63 100644 --- a/ui/go.mod +++ b/ui/go.mod @@ -5,8 +5,11 @@ go 1.19 require ( github.com/a-h/templ v0.2.408 github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/google/uuid v1.1.2 github.com/spf13/viper v1.17.0 go.mongodb.org/mongo-driver v1.12.1 + golang.org/x/crypto v0.13.0 ) require ( @@ -34,6 +37,7 @@ require ( github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cast v1.5.1 // indirect @@ -48,11 +52,10 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.13.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.15.0 // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/ui/go.sum b/ui/go.sum index 7cac87d..54e21a5 100644 --- a/ui/go.sum +++ b/ui/go.sum @@ -84,6 +84,8 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -140,6 +142,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -195,6 +198,8 @@ github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9c github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= @@ -393,11 +398,14 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/ui/main.go b/ui/main.go index 97de0c0..a507c9c 100644 --- a/ui/main.go +++ b/ui/main.go @@ -1,11 +1,13 @@ package main import ( + "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config" "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/controllers" "github.com/gin-gonic/gin" ) func main() { + config.Init() r := gin.Default() controllers.BuildRouter(r) diff --git a/ui/templates/login_content.templ b/ui/templates/login_content.templ index a0c7a11..a94a66b 100644 --- a/ui/templates/login_content.templ +++ b/ui/templates/login_content.templ @@ -1,6 +1,6 @@ package templates -templ LoginContent() { +templ LoginContent(signup bool, errorMsg string) {
+ if errorMsg != "" { +
+
+ Error +
+
+

{ errorMsg }

+
+
+ }
Sign in
-
+
@@ -38,6 +56,7 @@ templ LoginContent() { class="border-0 px-3 py-3 placeholder-gray-400 text-gray-700 bg-white rounded text-sm shadow focus:outline-none focus:ring w-full" placeholder="Password" style="transition: all 0.15s ease 0s;" + name="password" />
@@ -52,22 +71,35 @@ templ LoginContent() {
-
-
- -
- Create new account +
+ if !signup { + + } + if signup { + + } else { + + }
diff --git a/ui/templates/login_page.templ b/ui/templates/login_page.templ index 6aa1480..f3893cb 100644 --- a/ui/templates/login_page.templ +++ b/ui/templates/login_page.templ @@ -1,12 +1,25 @@ package templates -templ LoginPage() { +templ LoginPage(errorMsg string) { @Head() @Nav(false) - @LoginContent() + @LoginContent(false, errorMsg) + + @Footer() + @toggleNavBar() + +} + +templ SignupPage(errorMsg string) { + + + @Head() + + @Nav(false) + @LoginContent(true, errorMsg) @Footer() @toggleNavBar()