diff --git a/backend/.env.example b/backend/.env.example index 2b6bbc0..169a6ff 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,4 +1,6 @@ # No double quote needed for the text. Database will fail. +PORT=1323 +GO_ENV=dev GQL_COMPLEXITY=10 PG_HOST=localhost PG_USER=your_db_user @@ -6,4 +8,5 @@ PG_PASSWORD=your_db_password PG_DBNAME=your_db_name PG_PORT=5432 PG_SSLMODE=disable +FL_JWT_SECRET=test FL_BATCH_DEFAULT_AMOUNT=10 \ No newline at end of file diff --git a/backend/db/migrations/20240704123000_create_users_and_cards_tables.sql b/backend/db/migrations/20240704123000_create_users_and_cards_tables.sql index 643e218..f876c54 100644 --- a/backend/db/migrations/20240704123000_create_users_and_cards_tables.sql +++ b/backend/db/migrations/20240704123000_create_users_and_cards_tables.sql @@ -5,11 +5,15 @@ CREATE TABLE IF NOT EXISTS users ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + google_id TEXT UNIQUE, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_users_id ON users(id); CREATE INDEX idx_users_name ON users(name); +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_google_id ON users(google_id); CREATE TABLE IF NOT EXISTS cardgroups ( diff --git a/backend/go.mod b/backend/go.mod index bbd87d6..08dac01 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -6,6 +6,8 @@ require ( github.com/99designs/gqlgen v0.17.49 github.com/caarlos0/env/v11 v11.1.0 github.com/go-playground/validator/v10 v10.22.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.0 github.com/graph-gophers/dataloader/v7 v7.1.0 github.com/joho/godotenv v1.5.1 @@ -46,7 +48,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect diff --git a/backend/go.sum b/backend/go.sum index de99081..8affce0 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -69,6 +69,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= diff --git a/backend/graph/db/model.go b/backend/graph/db/model.go index e31c45c..26c903a 100644 --- a/backend/graph/db/model.go +++ b/backend/graph/db/model.go @@ -9,6 +9,8 @@ import ( type User struct { ID int64 `gorm:"column:id;primaryKey" validate:"number"` Name string `gorm:"column:name;not null" validate:"required,fl_name"` + Email string `gorm:"column:email;not null" validate:"required,email"` + GoogleID string `gorm:"column:google_id" validate:"-"` Created time.Time `gorm:"column:created;autoCreateTime"` Updated time.Time `gorm:"column:updated;autoCreateTime"` CardGroups []Cardgroup `gorm:"many2many:cardgroup_users" validate:"-"` diff --git a/backend/graph/generated.go b/backend/graph/generated.go index dc41bf1..dcf04f9 100644 --- a/backend/graph/generated.go +++ b/backend/graph/generated.go @@ -186,6 +186,8 @@ type ComplexityRoot struct { User struct { CardGroups func(childComplexity int, first *int, after *int64, last *int, before *int64) int Created func(childComplexity int) int + Email func(childComplexity int) int + GoogleID func(childComplexity int) int ID func(childComplexity int) int Name func(childComplexity int) int Roles func(childComplexity int, first *int, after *int64, last *int, before *int64) int @@ -1065,6 +1067,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.User.Created(childComplexity), true + case "User.email": + if e.complexity.User.Email == nil { + break + } + + return e.complexity.User.Email(childComplexity), true + + case "User.google_id": + if e.complexity.User.GoogleID == nil { + break + } + + return e.complexity.User.GoogleID(childComplexity), true + case "User.id": if e.complexity.User.ID == nil { break @@ -3975,6 +3991,10 @@ func (ec *executionContext) fieldContext_Mutation_createUser(ctx context.Context return ec.fieldContext_User_id(ctx, field) case "name": return ec.fieldContext_User_name(ctx, field) + case "email": + return ec.fieldContext_User_email(ctx, field) + case "google_id": + return ec.fieldContext_User_google_id(ctx, field) case "created": return ec.fieldContext_User_created(ctx, field) case "updated": @@ -4041,6 +4061,10 @@ func (ec *executionContext) fieldContext_Mutation_updateUser(ctx context.Context return ec.fieldContext_User_id(ctx, field) case "name": return ec.fieldContext_User_name(ctx, field) + case "email": + return ec.fieldContext_User_email(ctx, field) + case "google_id": + return ec.fieldContext_User_google_id(ctx, field) case "created": return ec.fieldContext_User_created(ctx, field) case "updated": @@ -4471,6 +4495,10 @@ func (ec *executionContext) fieldContext_Mutation_assignRoleToUser(ctx context.C return ec.fieldContext_User_id(ctx, field) case "name": return ec.fieldContext_User_name(ctx, field) + case "email": + return ec.fieldContext_User_email(ctx, field) + case "google_id": + return ec.fieldContext_User_google_id(ctx, field) case "created": return ec.fieldContext_User_created(ctx, field) case "updated": @@ -4537,6 +4565,10 @@ func (ec *executionContext) fieldContext_Mutation_removeRoleFromUser(ctx context return ec.fieldContext_User_id(ctx, field) case "name": return ec.fieldContext_User_name(ctx, field) + case "email": + return ec.fieldContext_User_email(ctx, field) + case "google_id": + return ec.fieldContext_User_google_id(ctx, field) case "created": return ec.fieldContext_User_created(ctx, field) case "updated": @@ -5300,6 +5332,10 @@ func (ec *executionContext) fieldContext_Query_user(ctx context.Context, field g return ec.fieldContext_User_id(ctx, field) case "name": return ec.fieldContext_User_name(ctx, field) + case "email": + return ec.fieldContext_User_email(ctx, field) + case "google_id": + return ec.fieldContext_User_google_id(ctx, field) case "created": return ec.fieldContext_User_created(ctx, field) case "updated": @@ -7076,6 +7112,94 @@ func (ec *executionContext) fieldContext_User_name(_ context.Context, field grap return fc, nil } +func (ec *executionContext) _User_email(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_email(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Email, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_User_email(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "User", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _User_google_id(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_google_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.GoogleID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_User_google_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "User", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _User_created(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { fc, err := ec.fieldContext_User_created(ctx, field) if err != nil { @@ -7381,6 +7505,10 @@ func (ec *executionContext) fieldContext_UserConnection_nodes(_ context.Context, return ec.fieldContext_User_id(ctx, field) case "name": return ec.fieldContext_User_name(ctx, field) + case "email": + return ec.fieldContext_User_email(ctx, field) + case "google_id": + return ec.fieldContext_User_google_id(ctx, field) case "created": return ec.fieldContext_User_created(ctx, field) case "updated": @@ -7581,6 +7709,10 @@ func (ec *executionContext) fieldContext_UserEdge_node(_ context.Context, field return ec.fieldContext_User_id(ctx, field) case "name": return ec.fieldContext_User_name(ctx, field) + case "email": + return ec.fieldContext_User_email(ctx, field) + case "google_id": + return ec.fieldContext_User_google_id(ctx, field) case "created": return ec.fieldContext_User_created(ctx, field) case "updated": @@ -9607,7 +9739,7 @@ func (ec *executionContext) unmarshalInputNewUser(ctx context.Context, obj inter asMap[k] = v } - fieldsInOrder := [...]string{"name", "role_ids", "created", "updated"} + fieldsInOrder := [...]string{"name", "email", "google_id", "role_ids", "created", "updated"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -9621,6 +9753,20 @@ func (ec *executionContext) unmarshalInputNewUser(ctx context.Context, obj inter return it, err } it.Name = data + case "email": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Email = data + case "google_id": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("google_id")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.GoogleID = data case "role_ids": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("role_ids")) data, err := ec.unmarshalNID2ᚕint64ᚄ(ctx, v) @@ -10891,6 +11037,16 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "email": + out.Values[i] = ec._User_email(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "google_id": + out.Values[i] = ec._User_google_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "created": out.Values[i] = ec._User_created(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/backend/graph/model/models_gen.go b/backend/graph/model/models_gen.go index 2b6ba16..7b9376d 100644 --- a/backend/graph/model/models_gen.go +++ b/backend/graph/model/models_gen.go @@ -88,10 +88,12 @@ type NewSwipeRecord struct { } type NewUser struct { - Name string `json:"name" validate:"required,fl_name,min=1"` - RoleIds []int64 `json:"role_ids"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Name string `json:"name" validate:"required,fl_name,min=1"` + Email string `json:"email" validate:"required,email"` + GoogleID string `json:"google_id" validate:"-"` + RoleIds []int64 `json:"role_ids"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type PageInfo struct { @@ -154,6 +156,8 @@ type UpsertDictionary struct { type User struct { ID int64 `json:"id"` Name string `json:"name" validate:"required,fl_name,min=1"` + Email string `json:"email" validate:"required,email"` + GoogleID string `json:"google_id" validate:"-"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` CardGroups *CardGroupConnection `json:"cardGroups" validate:"-"` diff --git a/backend/graph/schema.graphqls b/backend/graph/schema.graphqls index b599029..d35ee0d 100644 --- a/backend/graph/schema.graphqls +++ b/backend/graph/schema.graphqls @@ -1,202 +1,206 @@ scalar Time directive @validation( - format: String + format: String ) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION | FIELD_DEFINITION type PageInfo { - endCursor: ID - hasNextPage: Boolean! - hasPreviousPage: Boolean! - startCursor: ID + endCursor: ID + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: ID } type Card { - id: ID! - front: String! @validation(format: "required,min=1") - back: String! @validation(format: "required,min=1") - review_date: Time! - interval_days: Int! @validation(format: "gte=1") - created: Time! - updated: Time! - cardGroupID: ID! - cardGroup: CardGroup! @validation(format: "-") + id: ID! + front: String! @validation(format: "required,min=1") + back: String! @validation(format: "required,min=1") + review_date: Time! + interval_days: Int! @validation(format: "gte=1") + created: Time! + updated: Time! + cardGroupID: ID! + cardGroup: CardGroup! @validation(format: "-") } type CardGroup { - id: ID! - name: String! @validation(format: "required,fl_name,min=1") - created: Time! - updated: Time! - cards(first: Int, after: ID, last: Int, before: ID): CardConnection! @validation(format: "-") - users(first: Int, after: ID, last: Int, before: ID): UserConnection! @validation(format: "-") + id: ID! + name: String! @validation(format: "required,fl_name,min=1") + created: Time! + updated: Time! + cards(first: Int, after: ID, last: Int, before: ID): CardConnection! @validation(format: "-") + users(first: Int, after: ID, last: Int, before: ID): UserConnection! @validation(format: "-") } type CardEdge { - cursor: ID! - node: Card! @validation(format: "-") + cursor: ID! + node: Card! @validation(format: "-") } type CardConnection { - edges: [CardEdge] @validation(format: "-") - nodes: [Card] @validation(format: "-") - pageInfo: PageInfo! - totalCount: Int! + edges: [CardEdge] @validation(format: "-") + nodes: [Card] @validation(format: "-") + pageInfo: PageInfo! + totalCount: Int! } type UserEdge { - cursor: ID! - node: User! @validation(format: "-") + cursor: ID! + node: User! @validation(format: "-") } type UserConnection { - edges: [UserEdge] @validation(format: "-") - nodes: [User] @validation(format: "-") - pageInfo: PageInfo! - totalCount: Int! + edges: [UserEdge] @validation(format: "-") + nodes: [User] @validation(format: "-") + pageInfo: PageInfo! + totalCount: Int! } type Role { - id: ID! - name: String! @validation(format: "required,fl_name,min=1") - created: Time! - updated: Time! - users(first: Int, after: ID, last: Int, before: ID): UserConnection! @validation(format: "-") + id: ID! + name: String! @validation(format: "required,fl_name,min=1") + created: Time! + updated: Time! + users(first: Int, after: ID, last: Int, before: ID): UserConnection! @validation(format: "-") } type User { - id: ID! - name: String! @validation(format: "required,fl_name,min=1") - created: Time! - updated: Time! - cardGroups(first: Int, after: ID, last: Int, before: ID): CardGroupConnection! @validation(format: "-") - roles(first: Int, after: ID, last: Int, before: ID): RoleConnection! @validation(format: "-") + id: ID! + name: String! @validation(format: "required,fl_name,min=1") + email: String! @validation(format: "required,email") + google_id: String! @validation(format: "-") + created: Time! + updated: Time! + cardGroups(first: Int, after: ID, last: Int, before: ID): CardGroupConnection! @validation(format: "-") + roles(first: Int, after: ID, last: Int, before: ID): RoleConnection! @validation(format: "-") } type RoleEdge { - cursor: ID! - node: Role! @validation(format: "-") + cursor: ID! + node: Role! @validation(format: "-") } type RoleConnection { - edges: [RoleEdge] @validation(format: "-") - nodes: [Role] @validation(format: "-") - pageInfo: PageInfo! - totalCount: Int! + edges: [RoleEdge] @validation(format: "-") + nodes: [Role] @validation(format: "-") + pageInfo: PageInfo! + totalCount: Int! } type CardGroupEdge { - cursor: ID! - node: CardGroup! @validation(format: "-") + cursor: ID! + node: CardGroup! @validation(format: "-") } type CardGroupConnection { - edges: [CardGroupEdge] @validation(format: "-") - nodes: [CardGroup] @validation(format: "-") - pageInfo: PageInfo! - totalCount: Int! + edges: [CardGroupEdge] @validation(format: "-") + nodes: [CardGroup] @validation(format: "-") + pageInfo: PageInfo! + totalCount: Int! } type SwipeRecord { - id: ID! - userId: ID! - cardId: ID! - cardGroupID: ID! - mode: Int! @validation(format: "gte=0") - created: Time! - updated: Time! + id: ID! + userId: ID! + cardId: ID! + cardGroupID: ID! + mode: Int! @validation(format: "gte=0") + created: Time! + updated: Time! } type SwipeRecordEdge { - cursor: ID! - node: SwipeRecord! @validation(format: "-") + cursor: ID! + node: SwipeRecord! @validation(format: "-") } type SwipeRecordConnection { - edges: [SwipeRecordEdge] @validation(format: "-") - nodes: [SwipeRecord] @validation(format: "-") - pageInfo: PageInfo! - totalCount: Int! + edges: [SwipeRecordEdge] @validation(format: "-") + nodes: [SwipeRecord] @validation(format: "-") + pageInfo: PageInfo! + totalCount: Int! } input NewCard { - front: String! @validation(format: "required,min=1") - back: String! @validation(format: "required,min=1") - review_date: Time! - interval_days: Int = 1 @validation(format: "gte=1") - cardgroup_id: ID!, - created: Time!, - updated: Time!, + front: String! @validation(format: "required,min=1") + back: String! @validation(format: "required,min=1") + review_date: Time! + interval_days: Int = 1 @validation(format: "gte=1") + cardgroup_id: ID!, + created: Time!, + updated: Time!, } input NewCardGroup { - name: String! @validation(format: "required,min=1") - card_ids: [ID!] - user_ids: [ID!]! - created: Time!, - updated: Time!, + name: String! @validation(format: "required,min=1") + card_ids: [ID!] + user_ids: [ID!]! + created: Time!, + updated: Time!, } input NewUser { - name: String! @validation(format: "required,fl_name,min=1") - role_ids: [ID!]! - created: Time!, - updated: Time!, + name: String! @validation(format: "required,fl_name,min=1") + email: String! @validation(format: "required,email") + google_id: String! @validation(format: "-") + role_ids: [ID!]! + created: Time!, + updated: Time!, } input NewRole { - name: String! @validation(format: "required,fl_name,min=1") - created: Time!, - updated: Time!, + name: String! @validation(format: "required,fl_name,min=1") + created: Time!, + updated: Time!, } input NewSwipeRecord { - userId: ID! @validation(format: "required") - cardId: ID! @validation(format: "required") - cardGroupID: ID! @validation(format: "required") - mode: Int! @validation(format: "gte=0") - created: Time! - updated: Time! + userId: ID! @validation(format: "required") + cardId: ID! @validation(format: "required") + cardGroupID: ID! @validation(format: "required") + mode: Int! @validation(format: "gte=0") + created: Time! + updated: Time! } input UpsertDictionary { - cardgroup_id: ID!, - dictionary: String! @validation(format: "required") + cardgroup_id: ID!, + dictionary: String! @validation(format: "required") } type Query { - card(id: ID!): Card - cardGroup(id: ID!): CardGroup - role(id: ID!): Role - user(id: ID!): User - swipeRecord(id: ID!): SwipeRecord - cardsByCardGroup(cardGroupID: ID!, first: Int, after: ID, last: Int, before: ID): CardConnection - userRole(userID: ID!): Role - cardGroupsByUser(userID: ID!, first: Int, after: ID, last: Int, before: ID): CardGroupConnection - usersByRole(roleID: ID!, first: Int, after: ID, last: Int, before: ID): UserConnection - swipeRecords(userID: ID!,first: Int, after: ID, last: Int, before: ID): SwipeRecordConnection + card(id: ID!): Card + cardGroup(id: ID!): CardGroup + role(id: ID!): Role + user(id: ID!): User + swipeRecord(id: ID!): SwipeRecord + cardsByCardGroup(cardGroupID: ID!, first: Int, after: ID, last: Int, before: ID): CardConnection + userRole(userID: ID!): Role + cardGroupsByUser(userID: ID!, first: Int, after: ID, last: Int, before: ID): CardGroupConnection + usersByRole(roleID: ID!, first: Int, after: ID, last: Int, before: ID): UserConnection + swipeRecords(userID: ID!,first: Int, after: ID, last: Int, before: ID): SwipeRecordConnection } type Mutation { - createCard(input: NewCard!): Card - updateCard(id: ID!, input: NewCard!): Card - deleteCard(id: ID!): Boolean - createCardGroup(input: NewCardGroup!): CardGroup - updateCardGroup(id: ID!, input: NewCardGroup!): CardGroup - deleteCardGroup(id: ID!): Boolean - createUser(input: NewUser!): User - updateUser(id: ID!, input: NewUser!): User - deleteUser(id: ID!): Boolean - createRole(input: NewRole!): Role - updateRole(id: ID!, input: NewRole!): Role - deleteRole(id: ID!): Boolean - addUserToCardGroup(userID: ID!, cardGroupID: ID!): CardGroup - removeUserFromCardGroup(userID: ID!, cardGroupID: ID!): CardGroup - assignRoleToUser(userID: ID!, roleID: ID!): User - removeRoleFromUser(userID: ID!, roleID: ID!): User - createSwipeRecord(input: NewSwipeRecord!): SwipeRecord - updateSwipeRecord(id: ID!, input: NewSwipeRecord!): SwipeRecord - deleteSwipeRecord(id: ID!): Boolean - upsertDictionary(input: UpsertDictionary!): CardConnection - handleSwipe(input: NewSwipeRecord!): [Card!]! + createCard(input: NewCard!): Card + updateCard(id: ID!, input: NewCard!): Card + deleteCard(id: ID!): Boolean + createCardGroup(input: NewCardGroup!): CardGroup + updateCardGroup(id: ID!, input: NewCardGroup!): CardGroup + deleteCardGroup(id: ID!): Boolean + createUser(input: NewUser!): User + updateUser(id: ID!, input: NewUser!): User + deleteUser(id: ID!): Boolean + createRole(input: NewRole!): Role + updateRole(id: ID!, input: NewRole!): Role + deleteRole(id: ID!): Boolean + addUserToCardGroup(userID: ID!, cardGroupID: ID!): CardGroup + removeUserFromCardGroup(userID: ID!, cardGroupID: ID!): CardGroup + assignRoleToUser(userID: ID!, roleID: ID!): User + removeRoleFromUser(userID: ID!, roleID: ID!): User + createSwipeRecord(input: NewSwipeRecord!): SwipeRecord + updateSwipeRecord(id: ID!, input: NewSwipeRecord!): SwipeRecord + deleteSwipeRecord(id: ID!): Boolean + upsertDictionary(input: UpsertDictionary!): CardConnection + handleSwipe(input: NewSwipeRecord!): [Card!]! } diff --git a/backend/graph/schema.resolvers_test.go b/backend/graph/schema.resolvers_test.go index 021710b..32900a1 100644 --- a/backend/graph/schema.resolvers_test.go +++ b/backend/graph/schema.resolvers_test.go @@ -276,9 +276,11 @@ func TestGraphQLQueries(t *testing.T) { // Create an existing user user := repository.User{ - Name: "Users Query Test User", - Created: now, - Updated: now, + Name: "Users Query Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: now, + Updated: now, } if err := db.Create(&user).Error; err != nil { t.Fatalf("failed to create user: %v", err) @@ -443,9 +445,11 @@ func TestGraphQLQueries(t *testing.T) { // Create an existing user user := repository.User{ - Name: "Test User", - Created: now, - Updated: now, + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: now, + Updated: now, } if err := db.Create(&user).Error; err != nil { t.Fatalf("failed to create user: %v", err) @@ -504,9 +508,11 @@ func TestGraphQLQueries(t *testing.T) { // Create an existing user user := repository.User{ - Name: "Test User", - Created: now, - Updated: now, + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: now, + Updated: now, } if err := db.Create(&user).Error; err != nil { t.Fatalf("failed to create user: %v", err) @@ -581,9 +587,11 @@ func TestGraphQLQueries(t *testing.T) { // Create an existing user user := repository.User{ - Name: "Test User", - Created: now, - Updated: now, + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: now, + Updated: now, } if err := db.Create(&user).Error; err != nil { t.Fatalf("failed to create user: %v", err) @@ -847,9 +855,11 @@ func TestGraphQLQueries(t *testing.T) { // Create an existing user user := repository.User{ - Name: "Users Query Test User", - Created: now, - Updated: now, + Name: "Users Query Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: now, + Updated: now, } if err := db.Create(&user).Error; err != nil { t.Fatalf("failed to create user: %v", err) @@ -953,10 +963,12 @@ func TestGraphQLQueries(t *testing.T) { } input := model.NewUser{ - Name: "New User", - RoleIds: []int64{role.ID}, - Created: now, - Updated: now, + Name: "New User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + RoleIds: []int64{role.ID}, + Created: now, + Updated: now, } jsonInput, _ := json.Marshal(map[string]interface{}{ @@ -1009,9 +1021,11 @@ func TestGraphQLQueries(t *testing.T) { // Create an existing user user := repository.User{ - Name: "Old User", - Created: now, - Updated: now, + Name: "Old User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: now, + Updated: now, } if err := db.Create(&user).Error; err != nil { t.Fatalf("failed to create user: %v", err) @@ -1024,10 +1038,12 @@ func TestGraphQLQueries(t *testing.T) { // Input data for updating input := model.NewUser{ - Name: "Updated User", - RoleIds: []int64{role.ID}, - Created: now, - Updated: now, + Name: "Updated User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + RoleIds: []int64{role.ID}, + Created: now, + Updated: now, } // GraphQL Mutation test for updating user @@ -1063,9 +1079,11 @@ func TestGraphQLQueries(t *testing.T) { now := time.Now().UTC() user := repository.User{ - Name: "Test User", - Created: now, - Updated: now, + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: now, + Updated: now, } db.Create(&user) @@ -1232,9 +1250,11 @@ func TestGraphQLQueries(t *testing.T) { // Create an existing user user := repository.User{ - Name: "Test User", - Created: now, - Updated: now, + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: now, + Updated: now, } if err := db.Create(&user).Error; err != nil { t.Fatalf("failed to create user: %v", err) @@ -1304,9 +1324,11 @@ func TestGraphQLQueries(t *testing.T) { // Create an existing user user := repository.User{ - Name: "Test User", - Created: now, - Updated: now, + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: now, + Updated: now, } if err := db.Create(&user).Error; err != nil { t.Fatalf("failed to create user: %v", err) @@ -1377,9 +1399,11 @@ func TestGraphQLQueries(t *testing.T) { // Create an existing user user := repository.User{ - Name: "Test User", - Created: now, - Updated: now, + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: now, + Updated: now, } if err := db.Create(&user).Error; err != nil { t.Fatalf("failed to create user: %v", err) @@ -1442,9 +1466,11 @@ func TestGraphQLQueries(t *testing.T) { // Create an existing user user := repository.User{ - Name: "Test User", - Created: now, - Updated: now, + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: now, + Updated: now, } if err := db.Create(&user).Error; err != nil { t.Fatalf("failed to create user: %v", err) @@ -1809,10 +1835,12 @@ func TestGraphQLErrors(t *testing.T) { invalidID := "invalid-id" now := time.Now().UTC() input := model.NewUser{ - Name: "Updated User", - RoleIds: []int64{1, 2}, - Created: now, - Updated: now, + Name: "Updated User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + RoleIds: []int64{1, 2}, + Created: now, + Updated: now, } jsonInput, _ := json.Marshal(map[string]interface{}{ diff --git a/backend/graph/services/role_test.go b/backend/graph/services/role_test.go index e815ea0..18dcd38 100644 --- a/backend/graph/services/role_test.go +++ b/backend/graph/services/role_test.go @@ -147,7 +147,12 @@ func (suite *RoleTestSuite) TestRoleService() { suite.Run("Normal_AssignRoleToUser", func() { // Create a user and role - newUser := model.NewUser{Name: "Test User", Created: time.Now().UTC(), Updated: time.Now().UTC()} + newUser := model.NewUser{ + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: time.Now().UTC(), + Updated: time.Now().UTC()} createdUser, _ := userService.CreateUser(ctx, newUser) newRole := model.NewRole{Name: "Test Role"} @@ -171,7 +176,10 @@ func (suite *RoleTestSuite) TestRoleService() { suite.Run("Normal_RemoveRoleFromUser", func() { // Create a user and role - newUser := model.NewUser{Name: "Test User", Created: time.Now().UTC(), Updated: time.Now().UTC()} + newUser := model.NewUser{Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: time.Now().UTC(), Updated: time.Now().UTC()} createdUser, _ := userService.CreateUser(ctx, newUser) newRole := model.NewRole{Name: "Test Role"} diff --git a/backend/graph/services/user.go b/backend/graph/services/user.go index e1ea2ea..447092d 100644 --- a/backend/graph/services/user.go +++ b/backend/graph/services/user.go @@ -33,18 +33,22 @@ func NewUserService(db *gorm.DB, defaultLimit int) UserService { func ConvertToGormUserFromNew(input model.NewUser) *db.User { return &db.User{ - Name: input.Name, - Created: time.Now().UTC(), - Updated: time.Now().UTC(), + Name: input.Name, + Email: input.Email, + GoogleID: input.GoogleID, + Created: time.Now().UTC(), + Updated: time.Now().UTC(), } } func ConvertToUser(user db.User) *model.User { return &model.User{ - ID: user.ID, - Name: user.Name, - Created: user.Created, - Updated: user.Updated, + ID: user.ID, + Name: user.Name, + Email: user.Email, + GoogleID: user.GoogleID, + Created: user.Created, + Updated: user.Updated, } } diff --git a/backend/graph/services/user_test.go b/backend/graph/services/user_test.go index bfe08e6..098e65c 100644 --- a/backend/graph/services/user_test.go +++ b/backend/graph/services/user_test.go @@ -72,10 +72,12 @@ func (suite *UserTestSuite) TestUserService() { assert.NotNil(t, createdRole) input := model.NewUser{ - Name: "Test User", - Created: time.Now().UTC(), - Updated: time.Now().UTC(), - RoleIds: []int64{createdRole.ID}, + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: time.Now().UTC(), + Updated: time.Now().UTC(), + RoleIds: []int64{createdRole.ID}, } createdUser, err := userService.CreateUser(ctx, input) @@ -94,10 +96,12 @@ func (suite *UserTestSuite) TestUserService() { assert.NotNil(t, createdRole) input := model.NewUser{ - Name: "", // Invalid input - Created: time.Now().UTC(), - Updated: time.Now().UTC(), - RoleIds: []int64{createdRole.ID}, + Name: "", // Invalid input + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: time.Now().UTC(), + Updated: time.Now().UTC(), + RoleIds: []int64{createdRole.ID}, } createdUser, err := userService.CreateUser(ctx, input) @@ -116,10 +120,12 @@ func (suite *UserTestSuite) TestUserService() { assert.NotNil(t, createdRole) input := model.NewUser{ - Name: "Test User", - Created: time.Now().UTC(), - Updated: time.Now().UTC(), - RoleIds: []int64{createdRole.ID}, + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: time.Now().UTC(), + Updated: time.Now().UTC(), + RoleIds: []int64{createdRole.ID}, } createdUser, _ := userService.CreateUser(ctx, input) @@ -145,10 +151,12 @@ func (suite *UserTestSuite) TestUserService() { assert.NotNil(t, createdRole) input := model.NewUser{ - Name: "Test User", - Created: time.Now().UTC(), - Updated: time.Now().UTC(), - RoleIds: []int64{createdRole.ID}, + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: time.Now().UTC(), + Updated: time.Now().UTC(), + RoleIds: []int64{createdRole.ID}, } createdUser, _ := userService.CreateUser(ctx, input) @@ -161,7 +169,9 @@ func (suite *UserTestSuite) TestUserService() { suite.Run("Error_UpdateUser", func() { - updateInput := model.NewUser{Name: "Updated User"} + updateInput := model.NewUser{Name: "Updated User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7()} updatedUser, err := userService.UpdateUser(ctx, -1, updateInput) // Invalid ID assert.Error(t, err) @@ -179,10 +189,12 @@ func (suite *UserTestSuite) TestUserService() { assert.NotNil(t, createdRole) input := model.NewUser{ - Name: "Test User", - Created: time.Now().UTC(), - Updated: time.Now().UTC(), - RoleIds: []int64{createdRole.ID}, + Name: "Test User", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: time.Now().UTC(), + Updated: time.Now().UTC(), + RoleIds: []int64{createdRole.ID}, } createdUser, _ := userService.CreateUser(ctx, input) @@ -208,8 +220,14 @@ func (suite *UserTestSuite) TestUserService() { assert.NoError(t, err) assert.NotNil(t, createdRole) - input1 := model.NewUser{Name: "Test User 1", RoleIds: []int64{createdRole.ID}} - input2 := model.NewUser{Name: "Test User 2", RoleIds: []int64{createdRole.ID}} + input1 := model.NewUser{Name: "Test User 1", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + RoleIds: []int64{createdRole.ID}} + input2 := model.NewUser{Name: "Test User 2", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + RoleIds: []int64{createdRole.ID}} userService.CreateUser(ctx, input1) userService.CreateUser(ctx, input2) diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index 70c7fad..ec83662 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -30,7 +30,8 @@ type Config struct { PGQueryLimit int `env:"PG_QUERY_LIMIT,notEmpty" envDefault:"100"` // Application configuration - FLBatchDefaultAmount int `env:"FL_BATCH_DEFAULT_AMOUNT,notEmpty" envDefault:"10"` + JWTSecret string `env:"FL_JWT_SECRET,notEmpty" envDefault:"jwt_secret to be replaced."` + FLBatchDefaultAmount int `env:"FL_BATCH_DEFAULT_AMOUNT,notEmpty" envDefault:"10"` } // Cfg is the package-level variable that holds the parsed configuration diff --git a/backend/pkg/middlewares/jwt.go b/backend/pkg/middlewares/jwt.go new file mode 100644 index 0000000..982062d --- /dev/null +++ b/backend/pkg/middlewares/jwt.go @@ -0,0 +1,46 @@ +package middlewares + +import ( + "backend/pkg/config" + "github.com/golang-jwt/jwt/v5" + "github.com/labstack/echo/v4" + "net/http" + "strings" +) + +func JWTMiddleware(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + authHeader := c.Request().Header.Get("Authorization") + if authHeader == "" { + return echo.NewHTTPError(http.StatusUnauthorized, "Missing Authorization header") + } + + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + return echo.NewHTTPError(http.StatusUnauthorized, "Invalid Authorization header format") + } + + tokenStr := parts[1] + token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { + // Ensure that HMAC signing method is used + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Unexpected signing method") + } + return []byte(config.Cfg.JWTSecret), nil // Superbase's JWT signing key + }) + + if err != nil || !token.Valid { + return echo.NewHTTPError(http.StatusUnauthorized, "Invalid token") + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return echo.NewHTTPError(http.StatusUnauthorized, "Invalid token claims") + } + + // Add user information to the context + c.Set("user", claims) + + return next(c) + } +} diff --git a/backend/pkg/usecases/swipe_manager/swipe_manager_usecase_test.go b/backend/pkg/usecases/swipe_manager/swipe_manager_usecase_test.go index 67621f3..3bfbc8a 100644 --- a/backend/pkg/usecases/swipe_manager/swipe_manager_usecase_test.go +++ b/backend/pkg/usecases/swipe_manager/swipe_manager_usecase_test.go @@ -211,7 +211,7 @@ func (suite *SwipeManagerTestSuite) TestUpdateRecords() { suite.Run("Normal_GoodStateStrategy", func() { // Arrange - card, _, user, err := testutils.CreateUserCardAndCardGroup(ctx, + card, cardGroup, user, err := testutils.CreateUserCardAndCardGroup(ctx, suite.userService, suite.cardGroupService, suite.roleService, suite.cardService) assert.NoError(suite.T(), err) @@ -224,6 +224,15 @@ func (suite *SwipeManagerTestSuite) TestUpdateRecords() { for i := 0; i < config.Cfg.FLBatchDefaultAmount; i++ { mode := services.UNKNOWN // Set default mode to UNKNOWN + input := model.NewCard{ + Front: "Test Front" + strconv.Itoa(i), + Back: "Test Back" + strconv.Itoa(i), + ReviewDate: time.Now().UTC(), + CardgroupID: cardGroup.ID, + } + + _, err := suite.cardService.CreateCard(ctx, input) + // Randomly set 5 records to KNOWN if knownCount <= 5 && rng.Intn(config.Cfg. FLBatchDefaultAmount-knownCount) <= (5-knownCount) { diff --git a/backend/pkg/validator/validate_wrapper_test.go b/backend/pkg/validator/validate_wrapper_test.go index 6d41982..db4dce3 100644 --- a/backend/pkg/validator/validate_wrapper_test.go +++ b/backend/pkg/validator/validate_wrapper_test.go @@ -3,6 +3,7 @@ package validator_test import ( "backend/graph/db" "backend/pkg/validator" + "backend/testutils" "testing" "time" ) @@ -77,10 +78,12 @@ func TestModelValidation(t *testing.T) { { "Valid User", &db.User{ - ID: 1, - Name: "ValidName", - Created: time.Now().UTC(), - Updated: time.Now().UTC(), + ID: 1, + Name: "ValidName", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: time.Now().UTC(), + Updated: time.Now().UTC(), }, true, }, @@ -123,10 +126,12 @@ func TestModelValidation(t *testing.T) { { "Invalid User Name", &db.User{ - ID: 1, - Name: "Invalid Name!", - Created: time.Now().UTC(), - Updated: time.Now().UTC(), + ID: 1, + Name: "Invalid Name!", + Email: testutils.GetRandomEmail(8), + GoogleID: testutils.GenerateUUIDv7(), + Created: time.Now().UTC(), + Updated: time.Now().UTC(), }, false, }, diff --git a/backend/testutils/database.go b/backend/testutils/database.go index 024317c..7b2ffb0 100644 --- a/backend/testutils/database.go +++ b/backend/testutils/database.go @@ -119,7 +119,7 @@ func CreateUserAndCardGroup( cardGroupService services.CardGroupService, roleService services.RoleService) (*model.CardGroup, *model.User, error) { - randstr, err := CryptoRandString(8) + randstr := CryptoRandString(8) // Create a role newRole := model.NewRole{ @@ -132,10 +132,12 @@ func CreateUserAndCardGroup( // Create a user newUser := model.NewUser{ - Name: "Test User" + randstr, - Created: time.Now().UTC(), - Updated: time.Now().UTC(), - RoleIds: []int64{createdRole.ID}, // Assign the new role to the user + Name: "Test User" + randstr, + Email: GetRandomEmail(8), + GoogleID: GenerateUUIDv7(), + Created: time.Now().UTC(), + Updated: time.Now().UTC(), + RoleIds: []int64{createdRole.ID}, // Assign the new role to the user } createdUser, err := userService.CreateUser(ctx, newUser) if err != nil { @@ -178,7 +180,7 @@ func CreateUserCardAndCardGroup( } // Step 2: Create a Card - randstr, err := CryptoRandString(8) + randstr := CryptoRandString(8) if err != nil { return nil, nil, nil, goerr.Wrap(err, "failed to generate random string") } diff --git a/backend/testutils/tools.go b/backend/testutils/tools.go index 2153a8f..d649f7f 100644 --- a/backend/testutils/tools.go +++ b/backend/testutils/tools.go @@ -2,19 +2,35 @@ package testutils import ( "crypto/rand" + "fmt" + "github.com/google/uuid" "math/big" ) const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -func CryptoRandString(n int) (string, error) { +func CryptoRandString(n int) string { b := make([]byte, n) for i := range b { num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letterBytes)))) if err != nil { - return "", err + return "" } b[i] = letterBytes[num.Int64()] } - return string(b), nil + return string(b) +} + +func GetRandomEmail(n int) string { + domain := CryptoRandString(n) + name := CryptoRandString(n) + return fmt.Sprintf("%s@%s.com", name, domain) +} + +func GenerateUUIDv7() string { + id, err := uuid.NewV7() + if err != nil { + return "" + } + return id.String() }