diff --git a/enshi_back/REST_API_stuff/requestAnswers.go b/enshi_back/REST_API_stuff/requestAnswers.go index 58bc648..71b35f8 100644 --- a/enshi_back/REST_API_stuff/requestAnswers.go +++ b/enshi_back/REST_API_stuff/requestAnswers.go @@ -33,3 +33,10 @@ func ConflictAnswer(c *gin.Context, err error) { gin.H{"error": err.Error()}, ) } + +func UnauthorizedAnswer(c *gin.Context, err error) { + c.IndentedJSON( + http.StatusUnauthorized, + gin.H{"error": err.Error()}, + ) +} diff --git a/enshi_back/auth/jwt.go b/enshi_back/auth/jwt.go index 7c5964e..b956026 100644 --- a/enshi_back/auth/jwt.go +++ b/enshi_back/auth/jwt.go @@ -2,6 +2,7 @@ package auth import ( "fmt" + "strconv" "time" "github.com/golang-jwt/jwt/v5" @@ -12,10 +13,16 @@ var ( SecretKey string ) +type UserInfoJWT struct { + Id int64 + Username string + IsAdmin bool +} + // Generating new token with user info // // userInfo = { "id": int, "name": string } -func CreateToken(userInfo map[string]interface{}) (string, error) { +func CreateToken(userInfo UserInfoJWT) (string, error) { // Create new token token := jwt.New(jwt.SigningMethodHS256) @@ -23,8 +30,9 @@ func CreateToken(userInfo map[string]interface{}) (string, error) { claims := token.Claims.(jwt.MapClaims) // Add some info to claims - claims["name"] = userInfo["name"] - claims["id"] = userInfo["id"] + claims["username"] = userInfo.Username + claims["id"] = strconv.FormatInt(userInfo.Id, 10) + claims["isAdmin"] = userInfo.IsAdmin claims["exp"] = time.Now().Add(time.Hour * 1).Unix() // Get string token that will be passed to user diff --git a/enshi_back/db/go_queries/models.go b/enshi_back/db/go_queries/models.go index 0977118..7d227a9 100644 --- a/enshi_back/db/go_queries/models.go +++ b/enshi_back/db/go_queries/models.go @@ -52,7 +52,7 @@ type Like struct { type Post struct { PostID int64 `json:"post_id"` BlogID pgtype.Int8 `json:"blog_id"` - UserID pgtype.Int8 `json:"user_id"` + UserID int64 `json:"user_id"` Title pgtype.Text `json:"title"` Content pgtype.Text `json:"content"` CreatedAt pgtype.Timestamp `json:"created_at"` diff --git a/enshi_back/db/go_queries/posts_queries.sql.go b/enshi_back/db/go_queries/posts_queries.sql.go index d58cf1f..ba88faa 100644 --- a/enshi_back/db/go_queries/posts_queries.sql.go +++ b/enshi_back/db/go_queries/posts_queries.sql.go @@ -21,7 +21,7 @@ RETURNING post_id, blog_id, user_id, title, content, created_at, updated_at type CreatePostParams struct { PostID int64 `json:"post_id"` BlogID pgtype.Int8 `json:"blog_id"` - UserID pgtype.Int8 `json:"user_id"` + UserID int64 `json:"user_id"` Title pgtype.Text `json:"title"` Content pgtype.Text `json:"content"` } @@ -91,13 +91,34 @@ func (q *Queries) GetPostsByBlogId(ctx context.Context, blogID pgtype.Int8) ([]P return items, nil } +const getPostsByPostId = `-- name: GetPostsByPostId :one +SELECT post_id, blog_id, user_id, title, content, created_at, updated_at +FROM public.posts posts +where posts.post_id = $1 +` + +func (q *Queries) GetPostsByPostId(ctx context.Context, postID int64) (Post, error) { + row := q.db.QueryRow(ctx, getPostsByPostId, postID) + var i Post + err := row.Scan( + &i.PostID, + &i.BlogID, + &i.UserID, + &i.Title, + &i.Content, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const getPostsByUserId = `-- name: GetPostsByUserId :many SELECT post_id, blog_id, user_id, title, content, created_at, updated_at FROM public.posts posts where posts.user_id = $1 ` -func (q *Queries) GetPostsByUserId(ctx context.Context, userID pgtype.Int8) ([]Post, error) { +func (q *Queries) GetPostsByUserId(ctx context.Context, userID int64) ([]Post, error) { rows, err := q.db.Query(ctx, getPostsByUserId, userID) if err != nil { return nil, err @@ -134,7 +155,7 @@ RETURNING post_id, blog_id, user_id, title, content, created_at, updated_at type UpdatePostByPostIdParams struct { BlogID pgtype.Int8 `json:"blog_id"` - UserID pgtype.Int8 `json:"user_id"` + UserID int64 `json:"user_id"` Title pgtype.Text `json:"title"` Content pgtype.Text `json:"content"` PostID int64 `json:"post_id"` diff --git a/enshi_back/db/migrations/migration1.sql b/enshi_back/db/migrations/migration1.sql index a7870e0..7df83d3 100644 --- a/enshi_back/db/migrations/migration1.sql +++ b/enshi_back/db/migrations/migration1.sql @@ -9,7 +9,7 @@ CREATE TABLE "public"."users" ("user_id" bigint NOT NULL, "username" character v -- Create "blogs" table CREATE TABLE "public"."blogs" ("blog_id" bigint NOT NULL, "user_id" bigint NOT NULL, "title" character varying(255) NULL, "description" text NULL, "category_id" integer NULL, "created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("blog_id"), CONSTRAINT "blogs_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "public"."categories" ("category_id") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "blogs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE); -- Create "posts" table -CREATE TABLE "public"."posts" ("post_id" bigint NOT NULL, "blog_id" bigint NULL, "user_id" bigint NULL, "title" character varying(255) NULL, "content" text NULL, "created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("post_id"), CONSTRAINT "posts_blog_id_fkey" FOREIGN KEY ("blog_id") REFERENCES "public"."blogs" ("blog_id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "posts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE); +CREATE TABLE "public"."posts" ("post_id" bigint NOT NULL, "blog_id" bigint NULL, "user_id" bigint NOT NULL, "title" character varying(255) NULL, "content" text NULL, "created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("post_id"), CONSTRAINT "posts_blog_id_fkey" FOREIGN KEY ("blog_id") REFERENCES "public"."blogs" ("blog_id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "posts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE); -- Create "bookmarks" table CREATE TABLE "public"."bookmarks" ("user_id" bigint NOT NULL, "post_id" bigint NOT NULL, "bookmarked_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("user_id", "post_id"), CONSTRAINT "bookmarks_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "public"."posts" ("post_id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "bookmarks_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE); -- Create "comments" table diff --git a/enshi_back/db/queries/posts_queries.sql b/enshi_back/db/queries/posts_queries.sql index fa84843..c8e5545 100644 --- a/enshi_back/db/queries/posts_queries.sql +++ b/enshi_back/db/queries/posts_queries.sql @@ -1,3 +1,8 @@ +-- name: GetPostsByPostId :one +SELECT * +FROM public.posts posts +where posts.post_id = $1; + -- name: GetPostsByUserId :many SELECT * FROM public.posts posts diff --git a/enshi_back/db/sqlc.yml b/enshi_back/db/sqlc.yml index 850465f..cdfcea3 100644 --- a/enshi_back/db/sqlc.yml +++ b/enshi_back/db/sqlc.yml @@ -9,6 +9,7 @@ sql: package: "db_repo" out: "./go_queries" sql_package: "pgx/v5" + overrides: - column: users.password go_struct_tag: validate:"required" @@ -16,8 +17,8 @@ sql: go_struct_tag: validate:"required" - column: users.email go_struct_tag: validate:"required,email" + - db_type: "uuid" go_type: import: 'github.com/google/uuid' - type: 'UUID' - \ No newline at end of file + type: 'UUID' \ No newline at end of file diff --git a/enshi_back/middleware/adminMiddleware.go b/enshi_back/middleware/adminMiddleware.go new file mode 100644 index 0000000..d336e0b --- /dev/null +++ b/enshi_back/middleware/adminMiddleware.go @@ -0,0 +1,10 @@ +package middleware + +import "github.com/gin-gonic/gin" + +func AdminMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + + c.Next() + } +} diff --git a/enshi_back/middleware/authMiddleware.go b/enshi_back/middleware/authMiddleware.go index 1cfc670..d99a6df 100644 --- a/enshi_back/middleware/authMiddleware.go +++ b/enshi_back/middleware/authMiddleware.go @@ -10,19 +10,26 @@ import ( func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { - token := c.GetHeader("Authorization") - // tokenFromCoolies := c.Request.CookiesNamed("auth_cookie") + // token := c.GetHeader("Authorization") - claims, err := auth.ValidateToken(token) + tokenFromCookies := c.Request.CookiesNamed("auth_cookie")[0].Value + cookieClimes, err := auth.ValidateToken(tokenFromCookies) if err != nil { c.IndentedJSON(http.StatusUnauthorized, gin.H{"error auth": err.Error()}) c.Abort() return } + // claims, err := auth.ValidateToken(token) + // if err != nil { + // c.IndentedJSON(http.StatusUnauthorized, gin.H{"error auth": err.Error()}) + // c.Abort() + // return + // } + // Claims -> data stored in token - c.Set("id", claims["id"]) - c.Set("claims", claims) + c.Set(ContextUserId, cookieClimes["id"]) + c.Set(ContextTokenData, cookieClimes) c.Next() } diff --git a/enshi_back/middleware/corsMiddleware.go b/enshi_back/middleware/corsMiddleware.go index 872af6b..ad20f2d 100644 --- a/enshi_back/middleware/corsMiddleware.go +++ b/enshi_back/middleware/corsMiddleware.go @@ -6,7 +6,12 @@ func CORSMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, authorization, Authorization, accept, origin, Cache-Control, X-Requested-With, Cookie") + c.Writer.Header().Set( + "Access-Control-Allow-Headers", + "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, "+ + "authorization, Authorization, accept, origin, Cache-Control, "+ + "X-Requested-With, Cookie", + ) c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") c.Writer.Header().Set("Access-Control-Expose-Headers", "Access-Token, Uid, Authorization") diff --git a/enshi_back/middleware/getters/claims.go b/enshi_back/middleware/getters/claims.go new file mode 100644 index 0000000..c074cd4 --- /dev/null +++ b/enshi_back/middleware/getters/claims.go @@ -0,0 +1,44 @@ +package getters + +import ( + "enshi/auth" + "enshi/global" + "enshi/middleware" + "fmt" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +func GetClaimsFromContext(c *gin.Context) (auth.UserInfoJWT, error) { + var UserInfo auth.UserInfoJWT + + claims, exists := c.Get(middleware.ContextTokenData) + + if !exists { + return auth.UserInfoJWT{}, fmt.Errorf("error getting user id") + } + + parsedUserId, err := strconv.ParseInt( + claims.(jwt.MapClaims)["id"].(string), + 10, + 64, + ) + if err != nil { + return auth.UserInfoJWT{}, fmt.Errorf("error parsing user id") + } + + UserInfo.Id = parsedUserId + UserInfo.Username = claims.(jwt.MapClaims)["username"].(string) + isAdmin, err := strconv.ParseBool(claims.(jwt.MapClaims)["isAdmin"].(string)) + if err != nil { + UserInfo.IsAdmin = false + fmt.Println(global.RedColor + "isAdmin prop corrupted" + global.ResetColor) + } else { + UserInfo.IsAdmin = isAdmin + } + + return UserInfo, nil + +} diff --git a/enshi_back/middleware/getters/userId.go b/enshi_back/middleware/getters/userId.go new file mode 100644 index 0000000..f4a1319 --- /dev/null +++ b/enshi_back/middleware/getters/userId.go @@ -0,0 +1,23 @@ +package getters + +import ( + "enshi/middleware" + "fmt" + "strconv" + + "github.com/gin-gonic/gin" +) + +func GetUserIdFromContext(c *gin.Context) (int64, error) { + userId, exists := c.Get(middleware.ContextUserId) + + if !exists { + return -1, fmt.Errorf("error getting user id") + } + + if parsedUserId, err := strconv.ParseInt(userId.(string), 10, 64); err != nil { + return -1, fmt.Errorf("error parsing user id") + } else { + return parsedUserId, nil + } +} diff --git a/enshi_back/middleware/keys.go b/enshi_back/middleware/keys.go new file mode 100644 index 0000000..3035b5e --- /dev/null +++ b/enshi_back/middleware/keys.go @@ -0,0 +1,5 @@ +package middleware + +var ContextUserId = "id" +var ContextIsAdmin = "isAdmin" +var ContextTokenData = "tokenData" diff --git a/enshi_back/routes/createPost.go b/enshi_back/routes/createPost.go new file mode 100644 index 0000000..65dfc49 --- /dev/null +++ b/enshi_back/routes/createPost.go @@ -0,0 +1,44 @@ +package routes + +import ( + "context" + "encoding/binary" + rest_api_stuff "enshi/REST_API_stuff" + db_repo "enshi/db/go_queries" + "enshi/db_connection" + "enshi/middleware/getters" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func CreatePost(c *gin.Context) { + var postParams db_repo.CreatePostParams + + if err := c.BindJSON(&postParams); err != nil { + rest_api_stuff.BadRequestAnswer(c, err) + return + } + + userId, err := getters.GetUserIdFromContext(c) + if err != nil { + rest_api_stuff.BadRequestAnswer(c, err) + return + } + postParams.UserID = userId + + if uuidForPost, err := uuid.NewV7(); err != nil { + rest_api_stuff.InternalErrorAnswer(c, err) + return + } else { + postParams.PostID = -int64(binary.BigEndian.Uint64(uuidForPost[8:])) + } + + query := db_repo.New(db_connection.Dbx) + if _, err := query.CreatePost(context.Background(), postParams); err != nil { + rest_api_stuff.InternalErrorAnswer(c, err) + return + } + + rest_api_stuff.OkAnswer(c, "Post has been created!") +} diff --git a/enshi_back/routes/deletePost.go b/enshi_back/routes/deletePost.go new file mode 100644 index 0000000..29867e5 --- /dev/null +++ b/enshi_back/routes/deletePost.go @@ -0,0 +1,51 @@ +package routes + +import ( + "context" + rest_api_stuff "enshi/REST_API_stuff" + db_repo "enshi/db/go_queries" + "enshi/db_connection" + "enshi/middleware/getters" + "fmt" + + "github.com/gin-gonic/gin" +) + +func DeletePost(c *gin.Context) { + var deletePostId struct { + postId int64 + } + + if err := c.BindJSON(&deletePostId); err != nil { + rest_api_stuff.BadRequestAnswer(c, err) + return + } + + userClaims, err := getters.GetClaimsFromContext(c) + if err != nil { + rest_api_stuff.BadRequestAnswer(c, err) + return + } + + query := db_repo.New(db_connection.Dbx) + post, err := query.GetPostsByPostId(context.Background(), deletePostId.postId) + if err != nil { + rest_api_stuff.InternalErrorAnswer(c, err) + return + } + + if post.UserID != userClaims.Id { + rest_api_stuff.UnauthorizedAnswer(c, fmt.Errorf("you are not the author")) + return + } + + // TODO: Add block of code, so admin could delete anything + + err = query.DeletePostByPostId(context.Background(), deletePostId.postId) + if err != nil { + rest_api_stuff.InternalErrorAnswer(c, err) + return + } + + rest_api_stuff.OkAnswer(c, "post has been deleted") +} diff --git a/enshi_back/routes/login.go b/enshi_back/routes/login.go index 211ee47..6d4e3a7 100644 --- a/enshi_back/routes/login.go +++ b/enshi_back/routes/login.go @@ -8,7 +8,6 @@ import ( "enshi/global" "enshi/hasher" "net/http" - "strconv" "time" "github.com/gin-gonic/gin" @@ -16,7 +15,7 @@ import ( func Login(c *gin.Context) { type content struct { - Nickname string + Username string Password string } @@ -29,7 +28,7 @@ func Login(c *gin.Context) { } repo := db_repo.New(db_connection.Dbx) - user, err := repo.GetUserByUsername(context.Background(), body.Nickname) + user, err := repo.GetUserByUsername(context.Background(), body.Username) if err != nil { c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -47,20 +46,20 @@ func Login(c *gin.Context) { return } - user_info := map[string]interface{}{ - "id": user.UserID, - "name": user.Username, + userInfo := auth.UserInfoJWT{ + Id: user.UserID, + Username: user.Username, + IsAdmin: user.IsAdmin, } - token, err := auth.CreateToken(user_info) + token, err := auth.CreateToken(userInfo) if err != nil { c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } cookieName := "auth_cookie" - cookieValue := "id=" + strconv.FormatInt(user_info["id"].(int64), 10) + - "_nickname=" + user_info["name"].(string) + cookieValue := token maxAge := int(2 * time.Hour.Seconds()) // Cookie expiry time in seconds (1 hour) path := global.PathForCookies // Cookie path domain := global.DomainForCookies // Set domain (localhost for testing) diff --git a/enshi_back/routes/registerUser.go b/enshi_back/routes/registerUser.go index 91f5148..ffdc7cd 100644 --- a/enshi_back/routes/registerUser.go +++ b/enshi_back/routes/registerUser.go @@ -94,9 +94,10 @@ func RegisterUser(c *gin.Context) { return } - tokenParams := map[string]interface{}{ - "id": userParams.UserID, - "username": userParams.Username, + tokenParams := auth.UserInfoJWT{ + Id: userParams.UserID, + Username: userParams.Username, + IsAdmin: false, } token, err := auth.CreateToken(tokenParams) @@ -108,7 +109,7 @@ func RegisterUser(c *gin.Context) { cookieParams := &rest_api_stuff.CookieParams{ Name: "auth_cookie", Value: token, - MaxAge: int(time.Hour.Seconds() * 2), + MaxAge: int(2 * time.Hour.Seconds()), Path: global.PathForCookies, Domain: global.DomainForCookies, Secure: global.SecureForCookies, diff --git a/enshi_back/utils/routesSetup.go b/enshi_back/utils/routesSetup.go index ad97142..9bc1057 100644 --- a/enshi_back/utils/routesSetup.go +++ b/enshi_back/utils/routesSetup.go @@ -28,6 +28,11 @@ func SetupRotes(g *gin.Engine) error { // Auth group routes authGroup := g.Group("/") authGroup.Use(middleware.AuthMiddleware()) + authGroup.POST("createPost", routes.CreatePost) + authGroup.POST("deletePost", routes.DeletePost) + + adminGroup := authGroup.Group("/admin/") + adminGroup.Use(middleware.AdminMiddleware()) authGroup.POST("changeUserProfile", routes.ChangeUserProfile)