├── .gitignore ├── ssl └── .gitkeep ├── pkg ├── entity │ ├── go.sum │ ├── go.mod │ ├── user.go │ ├── bid.go │ ├── request.go │ └── auction.go ├── utils │ ├── go.mod │ └── arrays.go ├── httpserver │ ├── go.mod │ ├── option.go │ └── server.go ├── messaging │ ├── message.go │ ├── go.mod │ ├── publisher.go │ ├── subscriber.go │ └── kafka_test.go ├── pagination │ ├── types.go │ ├── gin_pagination.go │ └── go.mod ├── logger │ ├── go.mod │ ├── go.sum │ └── logger.go ├── postgres │ ├── option.go │ ├── go.mod │ └── postgres.go └── auth │ ├── types.go │ ├── gin_middleware.go │ ├── go.mod │ ├── auth_test.go │ └── auth.go ├── .DS_Store ├── clients ├── frontend │ ├── src │ │ ├── react-app-env.d.ts │ │ ├── assets │ │ │ ├── bid.png │ │ │ ├── auction.png │ │ │ └── loader.gif │ │ ├── constants.ts │ │ ├── common │ │ │ ├── loader │ │ │ │ └── Loader.tsx │ │ │ ├── table │ │ │ │ ├── styles.module.css │ │ │ │ └── index.tsx │ │ │ ├── table-row │ │ │ │ └── index.tsx │ │ │ └── pagination │ │ │ │ ├── styles.module.css │ │ │ │ └── index.tsx │ │ ├── types │ │ │ ├── bid.ts │ │ │ ├── user.ts │ │ │ ├── request.ts │ │ │ └── auction.ts │ │ ├── setupTests.ts │ │ ├── services │ │ │ ├── auth-header.ts │ │ │ ├── bid.ts │ │ │ ├── auth.ts │ │ │ └── request.ts │ │ ├── index.css │ │ ├── reportWebVitals.ts │ │ ├── components │ │ │ ├── Footer.tsx │ │ │ ├── ProfileImageBadge.tsx │ │ │ ├── AssignedAuction.tsx │ │ │ ├── MyBids.tsx │ │ │ ├── MyServiceRequests.tsx │ │ │ ├── MyRejectedRequests.tsx │ │ │ ├── ClosedAuctions.tsx │ │ │ ├── NewServiceRequests.tsx │ │ │ ├── MyAuctions.tsx │ │ │ ├── InProgressAuctions.tsx │ │ │ ├── OpenAuctions.tsx │ │ │ ├── AssignedAuctions.tsx │ │ │ ├── PendingAuctions.tsx │ │ │ ├── Assignments.tsx │ │ │ ├── AdminBoard.tsx │ │ │ └── CreateBid.tsx │ │ ├── index.tsx │ │ └── App.css │ ├── .prettierignore │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── index.html │ ├── prettierrc.json │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ └── README.md └── .DS_Store ├── scripts ├── Dockerfile ├── create_cert.sh ├── init_redis.sh ├── kafka │ └── docker-compose.yaml ├── db │ └── init_multiple_dbs.sh └── init_postgres.sh ├── services ├── user-service │ ├── migrations │ │ ├── 000002_create_users_data.down.sql │ │ ├── 000001_create_users_table.down.sql │ │ ├── 000001_create_users_table.up.sql │ │ └── 000002_create_users_data.up.sql │ ├── cmd │ │ └── app │ │ │ └── main.go │ ├── config │ │ ├── config.yaml │ │ └── config.go │ ├── internal │ │ ├── repository │ │ │ └── user │ │ │ │ ├── repository.go │ │ │ │ └── user_postgres.go │ │ ├── routes │ │ │ └── http │ │ │ │ └── routes.go │ │ ├── app │ │ │ └── app.go │ │ └── service │ │ │ └── user.go │ ├── Makefile │ └── go.mod ├── auction-service │ ├── migrations │ │ ├── 000001_create_auctions.down.sql │ │ └── 000001_create_auctions.up.sql │ ├── internal │ │ ├── events │ │ │ └── auction │ │ │ │ ├── events.go │ │ │ │ └── auction.go │ │ ├── repository │ │ │ ├── bid │ │ │ │ ├── repository.go │ │ │ │ └── bid_postgres.go │ │ │ └── auction │ │ │ │ └── repository.go │ │ ├── routes │ │ │ ├── events │ │ │ │ ├── events.go │ │ │ │ ├── bid │ │ │ │ │ └── controller.go │ │ │ │ └── request │ │ │ │ │ └── controller.go │ │ │ └── http │ │ │ │ └── routes.go │ │ ├── service │ │ │ └── bid.go │ │ └── app │ │ │ └── app.go │ ├── cmd │ │ └── app │ │ │ └── main.go │ ├── config │ │ ├── config.yaml │ │ └── config.go │ ├── Makefile │ └── go.mod ├── request-service │ ├── migrations │ │ ├── 000001_create_request_db.down.sql │ │ └── 000001_create_request_db.up.sql │ ├── internal │ │ ├── events │ │ │ └── request │ │ │ │ ├── events.go │ │ │ │ └── request.go │ │ ├── repository │ │ │ └── request │ │ │ │ └── repository.go │ │ ├── routes │ │ │ └── http │ │ │ │ └── routes.go │ │ └── app │ │ │ └── app.go │ ├── cmd │ │ └── app │ │ │ └── main.go │ └── config │ │ ├── config.yaml │ │ └── config.go └── bidding-service │ ├── migrations │ ├── 000001_create_bidding_db.down.sql │ └── 000001_create_bidding_db.up.sql │ ├── internal │ ├── events │ │ └── bid │ │ │ ├── events.go │ │ │ └── bid.go │ ├── repository │ │ ├── auction │ │ │ ├── repository.go │ │ │ └── auction_postgres.go │ │ └── bid │ │ │ └── repository.go │ ├── routes │ │ ├── events │ │ │ ├── events.go │ │ │ ├── request │ │ │ │ └── controller.go │ │ │ └── auction │ │ │ │ └── controller.go │ │ └── http │ │ │ └── routes.go │ ├── service │ │ ├── auction.go │ │ └── bid.go │ └── app │ │ └── app.go │ ├── cmd │ └── app │ │ └── main.go │ ├── config │ ├── config.yaml │ └── config.go │ ├── Makefile │ └── go.mod ├── demo ├── data │ ├── bid.go │ ├── user.go │ └── request.go ├── Makefile ├── go.mod ├── auction.go ├── bid.go ├── user.go └── request.go ├── integration-test ├── testdata │ ├── bid.go │ ├── user.go │ ├── auction.go │ └── request.go ├── Makefile └── go.mod ├── go.work ├── Dockerfile.demo ├── Dockerfile.integration ├── Dockerfile.service ├── LICENSE ├── api-gateway ├── api_proxy.conf └── nginx.conf └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ssl -------------------------------------------------------------------------------- /ssl/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/entity/go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/entity/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/pkg/entity 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /pkg/utils/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/pkg/utils 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PanGan21/service-bid-platform/HEAD/.DS_Store -------------------------------------------------------------------------------- /clients/frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /pkg/httpserver/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/pkg/httpserver 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /clients/frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Ignore artifacts: 3 | build 4 | coverage -------------------------------------------------------------------------------- /scripts/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres 2 | COPY ./db/init_multiple_dbs.sh /docker-entrypoint-initdb.d/ -------------------------------------------------------------------------------- /services/user-service/migrations/000002_create_users_data.down.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM users WHERE Id = 0; -------------------------------------------------------------------------------- /clients/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PanGan21/service-bid-platform/HEAD/clients/.DS_Store -------------------------------------------------------------------------------- /services/auction-service/migrations/000001_create_auctions.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS auctions; 2 | -------------------------------------------------------------------------------- /services/user-service/migrations/000001_create_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | -------------------------------------------------------------------------------- /services/request-service/migrations/000001_create_request_db.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS requests; 2 | -------------------------------------------------------------------------------- /clients/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /demo/data/bid.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | type BidData struct { 4 | Amount float64 5 | AuctionId int 6 | } 7 | -------------------------------------------------------------------------------- /integration-test/testdata/bid.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | var MockBid = map[string]interface{}{"AuctionId": 0, "Amount": 100.0} 4 | -------------------------------------------------------------------------------- /pkg/messaging/message.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | type Message struct { 4 | Payload interface{} 5 | Timestamp int64 6 | } 7 | -------------------------------------------------------------------------------- /clients/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PanGan21/service-bid-platform/HEAD/clients/frontend/public/favicon.ico -------------------------------------------------------------------------------- /clients/frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PanGan21/service-bid-platform/HEAD/clients/frontend/public/logo192.png -------------------------------------------------------------------------------- /clients/frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PanGan21/service-bid-platform/HEAD/clients/frontend/public/logo512.png -------------------------------------------------------------------------------- /clients/frontend/src/assets/bid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PanGan21/service-bid-platform/HEAD/clients/frontend/src/assets/bid.png -------------------------------------------------------------------------------- /clients/frontend/prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /clients/frontend/src/assets/auction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PanGan21/service-bid-platform/HEAD/clients/frontend/src/assets/auction.png -------------------------------------------------------------------------------- /clients/frontend/src/assets/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PanGan21/service-bid-platform/HEAD/clients/frontend/src/assets/loader.gif -------------------------------------------------------------------------------- /services/bidding-service/migrations/000001_create_bidding_db.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS auctions; 2 | 3 | DROP TABLE IF EXISTS bids; 4 | -------------------------------------------------------------------------------- /clients/frontend/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const API_URL = process.env.REACT_APP_API_URL || "http://localhost"; 2 | export const ROWS_PER_TABLE_PAGE = 10; 3 | -------------------------------------------------------------------------------- /pkg/pagination/types.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | type Pagination struct { 4 | Limit int `json:"limit"` 5 | Page int `json:"page"` 6 | Asc bool `json:"asc"` 7 | } 8 | -------------------------------------------------------------------------------- /clients/frontend/src/common/loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import loader from "../../assets/loader.gif"; 2 | 3 | export const Loader = () => { 4 | return Loader; 5 | }; 6 | -------------------------------------------------------------------------------- /services/bidding-service/internal/events/bid/events.go: -------------------------------------------------------------------------------- 1 | package bid 2 | 3 | import "github.com/PanGan21/pkg/entity" 4 | 5 | type BidEvents interface { 6 | PublishBidCreated(bid *entity.Bid) error 7 | } 8 | -------------------------------------------------------------------------------- /services/auction-service/internal/events/auction/events.go: -------------------------------------------------------------------------------- 1 | package auction 2 | 3 | import "github.com/PanGan21/pkg/entity" 4 | 5 | type AuctionEvents interface { 6 | PublishAuctionUpdated(auction *entity.Auction) error 7 | } 8 | -------------------------------------------------------------------------------- /scripts/create_cert.sh: -------------------------------------------------------------------------------- 1 | openssl req -x509 -nodes -newkey rsa:2048 -keyout key.pem -out cert.pem -sha256 -days 365 \ 2 | -subj "/C=GB/ST=London/L=London/O=Alros/OU=IT Department/CN=localhost" 3 | 4 | mv key.pem ssl/key.pem 5 | mv cert.pem ssl/cert.pem -------------------------------------------------------------------------------- /services/request-service/internal/events/request/events.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "github.com/PanGan21/pkg/entity" 4 | 5 | type RequestEvents interface { 6 | PublishRequestApproved(request *entity.Request, timestamp int64) error 7 | } 8 | -------------------------------------------------------------------------------- /clients/frontend/src/types/bid.ts: -------------------------------------------------------------------------------- 1 | export interface Bid { 2 | Id: number; 3 | Amount: number; 4 | CreatorId: string; 5 | AuctionId: string; 6 | } 7 | 8 | export interface NewBid { 9 | Amount: Bid["Amount"]; 10 | AuctionId: Bid["AuctionId"]; 11 | } 12 | -------------------------------------------------------------------------------- /clients/frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /services/request-service/migrations/000001_create_request_db.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS requests( 2 | Id SERIAL PRIMARY KEY, 3 | Title VARCHAR(255), 4 | Postcode VARCHAR(255), 5 | Info VARCHAR(255), 6 | CreatorId VARCHAR(255), 7 | Status VARCHAR(255), 8 | RejectionReason VARCHAR(255) 9 | ); -------------------------------------------------------------------------------- /pkg/logger/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/pkg/logger 2 | 3 | go 1.20 4 | 5 | require github.com/rs/zerolog v1.28.0 6 | 7 | require ( 8 | github.com/mattn/go-colorable v0.1.12 // indirect 9 | github.com/mattn/go-isatty v0.0.14 // indirect 10 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /clients/frontend/src/common/table/styles.module.css: -------------------------------------------------------------------------------- 1 | table { 2 | font-family: arial, sans-serif; 3 | border-collapse: collapse; 4 | width: 100%; 5 | } 6 | 7 | td, th { 8 | border: 1px solid #dddddd; 9 | text-align: left; 10 | padding: 8px; 11 | } 12 | 13 | tr:nth-child(even) { 14 | background-color: #dddddd; 15 | } 16 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.19 2 | 3 | use ( 4 | ./pkg/auth 5 | ./pkg/entity 6 | ./pkg/httpserver 7 | ./pkg/logger 8 | ./pkg/messaging 9 | ./pkg/pagination 10 | ./pkg/postgres 11 | ./pkg/utils 12 | ./services/bidding-service 13 | ./services/request-service 14 | ./services/auction-service 15 | ./services/user-service 16 | ./integration-test 17 | ./demo 18 | ) 19 | -------------------------------------------------------------------------------- /clients/frontend/src/services/auth-header.ts: -------------------------------------------------------------------------------- 1 | export default function authHeader() { 2 | const userStr = localStorage.getItem("user"); 3 | let user = null; 4 | if (userStr) user = JSON.parse(userStr); 5 | 6 | if (user && user.accessToken) { 7 | return { Cookie: "s.id " + user.accessToken }; 8 | } else { 9 | return { Cookie: "" }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /clients/frontend/src/types/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | Id: string; 3 | Username: string; 4 | Email: string; 5 | Phone: string; 6 | Password?: string; 7 | Roles: Array; 8 | } 9 | 10 | export interface UserDetails { 11 | Id: string; 12 | Username: string; 13 | Email: string; 14 | Phone: string; 15 | Roles: Array; 16 | } 17 | -------------------------------------------------------------------------------- /integration-test/testdata/user.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import "github.com/google/uuid" 4 | 5 | var DefaultRoles []int 6 | var MockUser = map[string]interface{}{"Username": uuid.New().String(), "Email": uuid.New().String(), "Phone": uuid.New().String(), "Password": "mockPassword"} 7 | var AdminUser = map[string]interface{}{"Username": "SuperAdmin", "Password": "password"} 8 | -------------------------------------------------------------------------------- /clients/frontend/src/types/request.ts: -------------------------------------------------------------------------------- 1 | export interface Request { 2 | Id: string; 3 | Title: string; 4 | CreatorId: string; 5 | Postcode: string; 6 | Info: string; 7 | Status: string; 8 | RejectionReason: string; 9 | } 10 | 11 | export interface NewRequest { 12 | Title: Request["Title"]; 13 | Postcode: Request["Postcode"]; 14 | Info: Request["Info"]; 15 | } 16 | -------------------------------------------------------------------------------- /services/user-service/cmd/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/PanGan21/user-service/config" 7 | "github.com/PanGan21/user-service/internal/app" 8 | ) 9 | 10 | func main() { 11 | // Configuration 12 | cfg, err := config.NewConfig() 13 | if err != nil { 14 | log.Fatalf("Config error: %s", err) 15 | } 16 | 17 | // Run 18 | app.Run(cfg) 19 | } 20 | -------------------------------------------------------------------------------- /demo/Makefile: -------------------------------------------------------------------------------- 1 | export 2 | 3 | # The output the help for each task 4 | .PHONY: help 5 | 6 | help: ## Display this help screen 7 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 8 | 9 | run: 10 | go run . 11 | .PHONY: run -------------------------------------------------------------------------------- /services/auction-service/cmd/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/PanGan21/auction-service/config" 7 | "github.com/PanGan21/auction-service/internal/app" 8 | ) 9 | 10 | func main() { 11 | // Configuration 12 | cfg, err := config.NewConfig() 13 | if err != nil { 14 | log.Fatalf("Config error: %s", err) 15 | } 16 | 17 | // Run 18 | app.Run(cfg) 19 | } 20 | -------------------------------------------------------------------------------- /services/bidding-service/cmd/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/PanGan21/bidding-service/config" 7 | "github.com/PanGan21/bidding-service/internal/app" 8 | ) 9 | 10 | func main() { 11 | // Configuration 12 | cfg, err := config.NewConfig() 13 | if err != nil { 14 | log.Fatalf("Config error: %s", err) 15 | } 16 | 17 | // Run 18 | app.Run(cfg) 19 | } 20 | -------------------------------------------------------------------------------- /services/request-service/cmd/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/PanGan21/request-service/config" 7 | "github.com/PanGan21/request-service/internal/app" 8 | ) 9 | 10 | func main() { 11 | // Configuration 12 | cfg, err := config.NewConfig() 13 | if err != nil { 14 | log.Fatalf("Config error: %s", err) 15 | } 16 | 17 | // Run 18 | app.Run(cfg) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/messaging/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/pkg/messaging 2 | 3 | go 1.20 4 | 5 | require github.com/segmentio/kafka-go v0.4.38 6 | 7 | require ( 8 | github.com/klauspost/compress v1.15.13 // indirect 9 | github.com/pierrec/lz4/v4 v4.1.17 // indirect 10 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 11 | golang.org/x/net v0.0.0-20220927171203-f486391704dc // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /services/bidding-service/internal/repository/auction/repository.go: -------------------------------------------------------------------------------- 1 | package auction 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/PanGan21/pkg/entity" 7 | ) 8 | 9 | type AuctionRepository interface { 10 | Create(ctx context.Context, auction entity.Auction) error 11 | UpdateOne(ctx context.Context, auction entity.Auction) error 12 | FindOneById(ctx context.Context, id int) (entity.Auction, error) 13 | } 14 | -------------------------------------------------------------------------------- /services/user-service/migrations/000001_create_users_table.up.sql: -------------------------------------------------------------------------------- 1 | -- User is a reserved keyword from postgres. 2 | -- Needs to be inside quotes 3 | CREATE TABLE IF NOT EXISTS users( 4 | Id SERIAL PRIMARY KEY, 5 | Username VARCHAR(255), 6 | Email VARCHAR(255), 7 | Phone VARCHAR(255), 8 | PasswordHash VARCHAR(255), 9 | Roles VARCHAR[], 10 | CONSTRAINT username_unique UNIQUE (Username) 11 | ); -------------------------------------------------------------------------------- /pkg/utils/arrays.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func Contains(s []string, e string) bool { 4 | for _, a := range s { 5 | if a == e { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | 12 | func Subslice(s1 []string, s2 []string) bool { 13 | if len(s1) > len(s2) { 14 | return false 15 | } 16 | for _, e := range s1 { 17 | if !Contains(s2, e) { 18 | return false 19 | } 20 | } 21 | return true 22 | } 23 | -------------------------------------------------------------------------------- /clients/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /clients/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /services/auction-service/config/config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | name: "auction-service" 3 | version: "1.0.0" 4 | 5 | http: 6 | port: "8000" 7 | session_secret: "secret" 8 | auth_secret: "auth_secret" 9 | 10 | logger: 11 | log_level: "debug" 12 | rollbar_env: "auction-service" 13 | 14 | postgres: 15 | pool_max: 2 16 | url: "postgres://postgres:password@localhost:5432/auction" 17 | 18 | kafka: 19 | retries: 3 20 | url: "kafka:9092" 21 | -------------------------------------------------------------------------------- /services/bidding-service/config/config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | name: "bidding-service" 3 | version: "1.0.0" 4 | 5 | http: 6 | port: "8000" 7 | session_secret: "secret" 8 | auth_secret: "auth_secret" 9 | 10 | logger: 11 | log_level: "debug" 12 | rollbar_env: "bidding-service" 13 | 14 | postgres: 15 | pool_max: 2 16 | url: "postgres://postgres:password@localhost:5432/bidding" 17 | 18 | kafka: 19 | retries: 3 20 | url: "kafka:9092" 21 | -------------------------------------------------------------------------------- /services/request-service/config/config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | name: "request-service" 3 | version: "1.0.0" 4 | 5 | http: 6 | port: "8000" 7 | session_secret: "secret" 8 | auth_secret: "auth_secret" 9 | 10 | logger: 11 | log_level: "debug" 12 | rollbar_env: "request-service" 13 | 14 | postgres: 15 | pool_max: 2 16 | url: "postgres://postgres:password@localhost:5432/request" 17 | 18 | kafka: 19 | retries: 3 20 | url: "kafka:9092" 21 | -------------------------------------------------------------------------------- /services/auction-service/internal/repository/bid/repository.go: -------------------------------------------------------------------------------- 1 | package bid 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/PanGan21/pkg/entity" 7 | ) 8 | 9 | type BidRepository interface { 10 | Create(ctx context.Context, bid entity.Bid) error 11 | FindManyByAuctionIdWithMinAmount(ctx context.Context, auctionId string) ([]entity.Bid, error) 12 | FindSecondMinAmountByAuctionId(ctx context.Context, auctionId string) (float64, error) 13 | } 14 | -------------------------------------------------------------------------------- /integration-test/testdata/auction.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | var MockAuction = map[string]interface{}{"Title": "mockTitle", "Postcode": "12345", "Info": "mockInfo", "Deadline": twoDaysAgo} 4 | var MockAuctionYesterday = map[string]interface{}{"Title": "mockTitle", "Postcode": "12345", "Info": "mockInfo", "Deadline": yesterday} 5 | var MockAuctionTomorrow = map[string]interface{}{"Title": "mockTitle", "Postcode": "12345", "Info": "mockInfo", "Deadline": tomorrow} 6 | -------------------------------------------------------------------------------- /services/user-service/config/config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | name: "user-service" 3 | version: "1.0.0" 4 | 5 | http: 6 | port: "8000" 7 | session_secret: "secret" 8 | auth_secret: "auth_secret" 9 | 10 | logger: 11 | log_level: "debug" 12 | rollbar_env: "user-service" 13 | 14 | redis: 15 | url: "localhost:6379" 16 | 17 | postgres: 18 | pool_max: 2 19 | url: "postgres://postgres:password@localhost:5432/user" 20 | 21 | user: 22 | password_salt: "salt" -------------------------------------------------------------------------------- /Dockerfile.demo: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.20.2-alpine3.16 AS builder 3 | WORKDIR /pkg 4 | COPY /pkg . 5 | COPY ./demo/go.mod ./demo/go.sum /modules/ 6 | WORKDIR /modules 7 | RUN go mod download 8 | 9 | # Run stage 10 | FROM golang:1.20.2-alpine3.16 11 | COPY --from=builder /pkg /pkg 12 | 13 | COPY ./demo /app 14 | WORKDIR /app 15 | 16 | RUN go env -w CGO_ENABLED=0 17 | RUN go env -w GOOS=linux 18 | RUN go env -w GOARCH=amd64 19 | 20 | CMD ["go", "run", "."] -------------------------------------------------------------------------------- /integration-test/Makefile: -------------------------------------------------------------------------------- 1 | export 2 | 3 | # The output the help for each task 4 | .PHONY: help 5 | 6 | help: ## Display this help screen 7 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 8 | 9 | integration-test: ### run integration-test 10 | go clean -testcache && go test -v ./... 11 | .PHONY: integration-test -------------------------------------------------------------------------------- /services/user-service/internal/repository/user/repository.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/PanGan21/pkg/entity" 7 | ) 8 | 9 | type UserRepository interface { 10 | GetByUsernameAndPassword(ctx context.Context, username string, password string) (entity.User, error) 11 | Create(ctx context.Context, username string, email string, phone string, passwordHash string, roles []string) (int, error) 12 | GetById(ctx context.Context, id int) (entity.User, error) 13 | } 14 | -------------------------------------------------------------------------------- /clients/frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /Dockerfile.integration: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.20.2-alpine3.16 AS builder 3 | WORKDIR /pkg 4 | COPY /pkg . 5 | COPY ./integration-test/go.mod ./integration-test/go.sum /modules/ 6 | WORKDIR /modules 7 | RUN go mod download 8 | 9 | # Run stage 10 | FROM golang:1.20.2-alpine3.16 11 | COPY --from=builder /pkg /pkg 12 | COPY --from=builder /go/pkg /go/pkg 13 | COPY ./integration-test /app 14 | WORKDIR /app 15 | 16 | RUN go env -w CGO_ENABLED=0 17 | RUN go env -w GOOS=linux 18 | RUN go env -w GOARCH=amd64 19 | 20 | CMD ["go", "test", "-v", "./..."] -------------------------------------------------------------------------------- /pkg/postgres/option.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import "time" 4 | 5 | // Option -. 6 | type Option func(*Postgres) 7 | 8 | // MaxPoolSize -. 9 | func MaxPoolSize(size int) Option { 10 | return func(c *Postgres) { 11 | c.maxPoolSize = size 12 | } 13 | } 14 | 15 | // ConnAttempts -. 16 | func ConnAttempts(attempts int) Option { 17 | return func(c *Postgres) { 18 | c.connAttempts = attempts 19 | } 20 | } 21 | 22 | // ConnTimeout -. 23 | func ConnTimeout(timeout time.Duration) Option { 24 | return func(c *Postgres) { 25 | c.connTimeout = timeout 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /services/auction-service/migrations/000001_create_auctions.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS auctions( 2 | Id SERIAL PRIMARY KEY, 3 | Title VARCHAR(255), 4 | Postcode VARCHAR(255), 5 | Info VARCHAR(255), 6 | CreatorId VARCHAR(255), 7 | Deadline BIGINT, 8 | Status VARCHAR(255), 9 | WinningBidId VARCHAR(255), 10 | WinnerId VARCHAR(255), 11 | WinningAmount FLOAT 12 | ); 13 | 14 | CREATE TABLE IF NOT EXISTS bids( 15 | Id INTEGER PRIMARY KEY, 16 | Amount FLOAT, 17 | CreatorId VARCHAR(255), 18 | AuctionId INTEGER REFERENCES auctions (Id) 19 | ); -------------------------------------------------------------------------------- /services/bidding-service/migrations/000001_create_bidding_db.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS auctions( 2 | Id INTEGER PRIMARY KEY, 3 | Title VARCHAR(255), 4 | Postcode VARCHAR(255), 5 | Info VARCHAR(255), 6 | CreatorId VARCHAR(255), 7 | Deadline BIGINT, 8 | Status VARCHAR(255), 9 | WinningBidId VARCHAR(255), 10 | WinnerId VARCHAR(255), 11 | WinningAmount FLOAT 12 | ); 13 | 14 | CREATE TABLE IF NOT EXISTS bids( 15 | Id SERIAL PRIMARY KEY, 16 | Amount FLOAT, 17 | CreatorId VARCHAR(255), 18 | AuctionId INTEGER REFERENCES auctions (Id) 19 | ); -------------------------------------------------------------------------------- /clients/frontend/src/common/table-row/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useState } from "react"; 2 | 3 | type Props = { 4 | children: ReactNode; 5 | onClick: React.MouseEventHandler; 6 | }; 7 | 8 | export const TableRow = ({ children, onClick }: Props) => { 9 | const [opacity, setOpacity] = useState(1); 10 | 11 | return ( 12 | setOpacity(0.5)} 15 | onMouseLeave={() => setOpacity(1)} 16 | onClick={onClick} 17 | > 18 | {children} 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /scripts/init_redis.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | set -eo pipefail 4 | 5 | # if a redis container is running, print instructions to kill it and exit 6 | RUNNING_CONTAINER=$(docker ps --filter 'name=redis' --format '{{.ID}}') 7 | if [[ -n $RUNNING_CONTAINER ]]; then 8 | echo >&2 "there is a redis container already running, kill it with" 9 | echo >&2 " docker kill ${RUNNING_CONTAINER}" 10 | exit 1 11 | fi 12 | 13 | # Launch Redis using Docker 14 | docker run \ 15 | -p "6379:6379" \ 16 | -d \ 17 | --name "redis_$(date '+%s')" \ 18 | redis:6 19 | 20 | >&2 echo "Redis is ready to go!" -------------------------------------------------------------------------------- /services/auction-service/Makefile: -------------------------------------------------------------------------------- 1 | export 2 | 3 | # The output the help for each task 4 | .PHONY: help 5 | 6 | help: ## Display this help screen 7 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 8 | 9 | run-local: ### Run backend locally 10 | go run cmd/app/main.go 11 | .PHONY: run 12 | 13 | migrate-up: ### migration up 14 | migrate -path migrations -database '$(PG_URL)?sslmode=disable/auction' up 15 | .PHONY: migrate-up 16 | 17 | 18 | -------------------------------------------------------------------------------- /scripts/kafka/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | zookeeper: 4 | image: wurstmeister/zookeeper 5 | container_name: zookeeper 6 | ports: 7 | - "2181:2181" 8 | kafka: 9 | image: wurstmeister/kafka 10 | container_name: kafka 11 | ports: 12 | - "9092:9092" 13 | environment: 14 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 15 | KAFKA_ADVERTISED_HOST_NAME: kafka 16 | KAFKA_ADVERTISED_PORT: 9092 17 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 18 | KAFKA_CREATE_TOPICS: "my-topic:3:1" 19 | # KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' -------------------------------------------------------------------------------- /clients/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /pkg/entity/user.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type PublicUser struct { 4 | Id string `json:"Id" db:"Id"` 5 | Username string `json:"Username" db:"Username"` 6 | } 7 | 8 | type UserDetails struct { 9 | Email string `json:"Email" db:"Email"` 10 | Phone string `json:"Phone" db:"Phone"` 11 | } 12 | 13 | type User struct { 14 | Id string `json:"Id" db:"Id"` 15 | Username string `json:"Username" db:"Username"` 16 | Email string `json:"Email" db:"Email"` 17 | Phone string `json:"Phone" db:"Phone"` 18 | PasswordHash string `json:"PasswordHash" db:"PasswordHash"` 19 | Roles []string `json:"Roles" db:"Roles"` 20 | } 21 | -------------------------------------------------------------------------------- /clients/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /clients/frontend/src/services/bid.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { API_URL } from "../constants"; 3 | import { NewBid } from "../types/bid"; 4 | 5 | export const createBid = async (newBid: NewBid) => { 6 | return axios.post(API_URL + "/bidding/", newBid, { withCredentials: true }); 7 | }; 8 | 9 | export const getMyBids = async (limit: number, page: number) => { 10 | return axios.get( 11 | API_URL + `/bidding/own?limit=${limit}&page=${page}&asc=false`, 12 | { 13 | withCredentials: true, 14 | } 15 | ); 16 | }; 17 | 18 | export const countMyBids = async () => { 19 | return axios.get(API_URL + "/bidding/count/own", { withCredentials: true }); 20 | }; 21 | -------------------------------------------------------------------------------- /services/bidding-service/internal/events/bid/bid.go: -------------------------------------------------------------------------------- 1 | package bid 2 | 3 | import ( 4 | "github.com/PanGan21/pkg/entity" 5 | "github.com/PanGan21/pkg/messaging" 6 | ) 7 | 8 | type bidEvents struct { 9 | pub messaging.Publisher 10 | } 11 | 12 | const ( 13 | BID_CREATED_TOPIC = "bid-created" 14 | ) 15 | 16 | func NewBidEvents(pub messaging.Publisher) *bidEvents { 17 | return &bidEvents{pub: pub} 18 | } 19 | 20 | func (events *bidEvents) PublishBidCreated(auction *entity.Bid) error { 21 | msg := messaging.Message{ 22 | Payload: auction, 23 | } 24 | 25 | err := events.pub.Publish(BID_CREATED_TOPIC, msg) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /services/bidding-service/internal/repository/bid/repository.go: -------------------------------------------------------------------------------- 1 | package bid 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/PanGan21/pkg/entity" 7 | "github.com/PanGan21/pkg/pagination" 8 | ) 9 | 10 | type BidRepository interface { 11 | Create(ctx context.Context, creatorId string, auctionId int, amount float64) (int, error) 12 | FindOneById(ctx context.Context, id int) (entity.Bid, error) 13 | FindByAuctionId(ctx context.Context, auctionId int, pagination *pagination.Pagination) (*[]entity.Bid, error) 14 | FindByCreatorId(ctx context.Context, creatorId string, pagination *pagination.Pagination) (*[]entity.Bid, error) 15 | CountByCreatorId(ctx context.Context, creatorId string) (int, error) 16 | } 17 | -------------------------------------------------------------------------------- /services/user-service/Makefile: -------------------------------------------------------------------------------- 1 | export 2 | 3 | # The output the help for each task 4 | .PHONY: help 5 | 6 | help: ## Display this help screen 7 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 8 | 9 | run-local: ### Run backend locally 10 | go run cmd/app/main.go 11 | .PHONY: run 12 | 13 | migrate-up: ### migration up 14 | migrate -path migrations -database '$(PG_URL)?sslmode=disable/user' up 15 | .PHONY: migrate-up 16 | 17 | # Create migration 18 | # migrate create -ext sql -dir migrations -seq create_users_table 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/data/user.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | type UserData struct { 4 | Username string 5 | Password string 6 | } 7 | 8 | var SuperAdmin = UserData{ 9 | Username: "SuperAdmin", 10 | Password: "password", 11 | } 12 | 13 | var Resident1User = UserData{ 14 | Username: "Resident1", 15 | Password: "password", 16 | } 17 | 18 | var Resident2User = UserData{ 19 | Username: "Resident1", 20 | Password: "password", 21 | } 22 | 23 | var Bidder1User = UserData{ 24 | Username: "Bidder1", 25 | Password: "password", 26 | } 27 | 28 | var Bidder2User = UserData{ 29 | Username: "Bidder2", 30 | Password: "password", 31 | } 32 | 33 | var Bidder3User = UserData{ 34 | Username: "Bidder3", 35 | Password: "password", 36 | } 37 | -------------------------------------------------------------------------------- /clients/frontend/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | export const Footer: React.FC = () => { 2 | return ( 3 |
12 |
13 | © {new Date().getFullYear()} Copyright:{" "} 14 | 20 | Panagiotis Ganelis 21 | 22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /integration-test/testdata/request.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import "time" 4 | 5 | var today = time.Now() 6 | var twoDaysAgo = today.AddDate(0, 0, -2).UTC().UnixMilli() 7 | var yesterday = today.AddDate(0, 0, -1).UTC().UnixMilli() 8 | var tomorrow = today.AddDate(0, 0, 1).UTC().UnixMilli() 9 | 10 | var MockRequest = map[string]interface{}{"Title": "mockTitle", "Postcode": "12345", "Info": "mockInfo"} 11 | var MockRequestYesterday = map[string]interface{}{"Title": "mockTitle", "Postcode": "12345", "Info": "mockInfo"} 12 | var MockRequestTomorrow = map[string]interface{}{"Title": "mockTitle", "Postcode": "12345", "Info": "mockInfo"} 13 | 14 | var MockRejectionReason = map[string]interface{}{"RejectionReason": "mockRejectionReason"} 15 | -------------------------------------------------------------------------------- /scripts/db/init_multiple_dbs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -u 5 | 6 | function create_user_and_database() { 7 | local database=$1 8 | echo " Creating user and database '$database'" 9 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 10 | CREATE USER $database; 11 | CREATE DATABASE $database; 12 | GRANT ALL PRIVILEGES ON DATABASE $database TO $database; 13 | EOSQL 14 | } 15 | 16 | if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then 17 | echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" 18 | for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do 19 | create_user_and_database $db 20 | done 21 | echo "Multiple databases created" 22 | fi -------------------------------------------------------------------------------- /services/auction-service/internal/events/auction/auction.go: -------------------------------------------------------------------------------- 1 | package auction 2 | 3 | import ( 4 | "github.com/PanGan21/pkg/entity" 5 | "github.com/PanGan21/pkg/messaging" 6 | ) 7 | 8 | type auctionEvents struct { 9 | pub messaging.Publisher 10 | } 11 | 12 | const ( 13 | AUCTION_UPDATED_TOPIC = "auction-updated" 14 | ) 15 | 16 | func NewAuctionEvents(pub messaging.Publisher) *auctionEvents { 17 | return &auctionEvents{pub: pub} 18 | } 19 | 20 | func (events *auctionEvents) PublishAuctionUpdated(auction *entity.Auction) error { 21 | msg := messaging.Message{ 22 | Payload: auction, 23 | } 24 | 25 | err := events.pub.Publish(AUCTION_UPDATED_TOPIC, msg) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /clients/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | import { BrowserRouter } from 'react-router-dom'; 7 | 8 | const root = ReactDOM.createRoot( 9 | document.getElementById('root') as HTMLElement 10 | ); 11 | root.render( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | // If you want to start measuring performance in your app, pass a function 20 | // to log results (for example: reportWebVitals(console.log)) 21 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 22 | reportWebVitals(); 23 | -------------------------------------------------------------------------------- /services/bidding-service/Makefile: -------------------------------------------------------------------------------- 1 | export 2 | 3 | # The output the help for each task 4 | .PHONY: help 5 | 6 | help: ## Display this help screen 7 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 8 | 9 | run-local: ### Run backend locally 10 | go run cmd/app/main.go 11 | .PHONY: run 12 | 13 | migrate-up: ### migration up 14 | migrate -path migrations -database '$(PG_URL)?sslmode=disable/bidding' up 15 | .PHONY: migrate-up 16 | 17 | migrate-create: ### migrate create new migration (up and down) 18 | migrate create -ext sql -dir migrations -seq bids 19 | .PHONY: migrate-create 20 | 21 | -------------------------------------------------------------------------------- /pkg/httpserver/option.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | // Option -. 9 | type Option func(*Server) 10 | 11 | // Port -. 12 | func Port(port string) Option { 13 | return func(s *Server) { 14 | s.server.Addr = net.JoinHostPort("", port) 15 | } 16 | } 17 | 18 | // ReadTimeout -. 19 | func ReadTimeout(timeout time.Duration) Option { 20 | return func(s *Server) { 21 | s.server.ReadTimeout = timeout 22 | } 23 | } 24 | 25 | // WriteTimeout -. 26 | func WriteTimeout(timeout time.Duration) Option { 27 | return func(s *Server) { 28 | s.server.WriteTimeout = timeout 29 | } 30 | } 31 | 32 | // ShutdownTimeout -. 33 | func ShutdownTimeout(timeout time.Duration) Option { 34 | return func(s *Server) { 35 | s.shutdownTimeout = timeout 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /clients/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | label { 2 | display: block; 3 | margin-top: 10px; 4 | } 5 | 6 | .card-container.card { 7 | max-width: 350px !important; 8 | padding: 40px 40px; 9 | } 10 | 11 | .card { 12 | background-color: #f7f7f7; 13 | padding: 20px 25px 30px; 14 | margin: 0 auto 25px; 15 | margin-top: 50px; 16 | -moz-border-radius: 2px; 17 | -webkit-border-radius: 2px; 18 | border-radius: 2px; 19 | -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 20 | -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 21 | box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 22 | } 23 | 24 | .profile-img-card { 25 | width: 96px; 26 | height: 96px; 27 | margin: 0 auto 10px; 28 | display: block; 29 | -moz-border-radius: 50%; 30 | -webkit-border-radius: 50%; 31 | border-radius: 50%; 32 | } 33 | -------------------------------------------------------------------------------- /Dockerfile.service: -------------------------------------------------------------------------------- 1 | # Step 1: Modules caching 2 | FROM golang:1.20.2-alpine3.16 AS modules 3 | WORKDIR /pkg 4 | COPY /pkg . 5 | ARG service_name 6 | COPY ./services/$service_name/go.mod ./services/$service_name/go.sum /modules/ 7 | WORKDIR /modules 8 | RUN go mod download 9 | 10 | # Step 2: Builder 11 | FROM golang:1.20.2-alpine3.16 as builder 12 | COPY --from=modules /pkg /pkg 13 | COPY --from=modules /go/pkg /go/pkg 14 | ARG service_name 15 | COPY ./services/$service_name /app 16 | WORKDIR /app 17 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ 18 | go build -tags migrate -o /bin/app ./cmd/app 19 | 20 | # Step 3: Final 21 | FROM scratch 22 | COPY --from=builder /app/config /config 23 | COPY --from=builder /app/migrations /migrations 24 | COPY --from=builder /bin/app /app 25 | CMD ["/app"] 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /services/request-service/internal/events/request/request.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "github.com/PanGan21/pkg/entity" 5 | "github.com/PanGan21/pkg/messaging" 6 | ) 7 | 8 | type requestEvents struct { 9 | pub messaging.Publisher 10 | } 11 | 12 | const ( 13 | REQUEST_APPROVED_TOPIC = "request-approved" 14 | REQUEST_UPDATED_TOPIC = "request-updated" 15 | ) 16 | 17 | func NewRequestEvents(pub messaging.Publisher) *requestEvents { 18 | return &requestEvents{pub: pub} 19 | } 20 | 21 | func (events *requestEvents) PublishRequestApproved(request *entity.Request, timestamp int64) error { 22 | msg := messaging.Message{ 23 | Payload: request, 24 | Timestamp: timestamp, 25 | } 26 | 27 | err := events.pub.Publish(REQUEST_APPROVED_TOPIC, msg) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/pagination/gin_pagination.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func GeneratePaginationFromRequest(c *gin.Context) Pagination { 11 | // Default 12 | limit := 2 13 | page := 1 14 | asc := true 15 | 16 | var err error 17 | 18 | query := c.Request.URL.Query() 19 | for key, value := range query { 20 | queryValue := value[len(value)-1] 21 | switch key { 22 | case "limit": 23 | limit, err = strconv.Atoi(queryValue) 24 | case "page": 25 | page, err = strconv.Atoi(queryValue) 26 | case "asc": 27 | asc, err = strconv.ParseBool(queryValue) 28 | 29 | } 30 | } 31 | if err != nil { 32 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Validation error"}) 33 | } 34 | 35 | return Pagination{ 36 | Limit: limit, 37 | Page: page, 38 | Asc: asc, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /clients/frontend/src/common/pagination/styles.module.css: -------------------------------------------------------------------------------- 1 | .pagination { 2 | margin: 30px 35px 0; 3 | text-align: right; 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: baseline; 7 | } 8 | 9 | .pageInfo { 10 | color: #a0a3bd; 11 | font-size: 0.874em; 12 | letter-spacing: 0.5px; 13 | } 14 | 15 | .pageButtons { 16 | display: flex; 17 | } 18 | 19 | .pageBtn { 20 | border: 1px solid #a0a3bd; 21 | color: #a0a3bd; 22 | border-radius: 5px; 23 | margin: 5px; 24 | width: 35px; 25 | height: 35px; 26 | font-weight: normal; 27 | font-size: 15px; 28 | } 29 | 30 | .activeBtn { 31 | border: 1px solid blue; 32 | color: blue; 33 | background-color: transparent; 34 | } 35 | 36 | .disabledPageBtn { 37 | background-color: #a0a3bd; 38 | cursor: not-allowed; 39 | opacity: 0.5; 40 | } -------------------------------------------------------------------------------- /pkg/postgres/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/pkg/postgres 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/Masterminds/squirrel v1.5.3 7 | github.com/jackc/pgx/v4 v4.17.2 8 | ) 9 | 10 | require ( 11 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 12 | github.com/jackc/pgconn v1.13.0 // indirect 13 | github.com/jackc/pgio v1.0.0 // indirect 14 | github.com/jackc/pgpassfile v1.0.0 // indirect 15 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 16 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 17 | github.com/jackc/pgtype v1.12.0 // indirect 18 | github.com/jackc/puddle v1.3.0 // indirect 19 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 20 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 21 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 22 | golang.org/x/text v0.3.7 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /demo/data/request.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | type RequestData struct { 4 | Title string 5 | Postcode string 6 | Info string 7 | } 8 | 9 | var YesterdayRequestNew = RequestData{ 10 | Title: "Καθαρισμός οικοπέδου", 11 | Postcode: "19007", 12 | Info: "150 τ.μ. κόψιμο κλαδιών, όργωμα", 13 | } 14 | 15 | var TwoDaysAgoRequestNew = RequestData{ 16 | Title: "Κόψιμο δέντρου", 17 | Postcode: "15387", 18 | Info: "Πεύκο που καλύπτει το σπίτι πρέπει να κοπεί. Περίπου 5 μέτρα ύψος.", 19 | } 20 | 21 | var TwoDaysAgoRequestOpen = RequestData{ 22 | Title: "Καθαρισμός σκουπιδιών", 23 | Postcode: "14660", 24 | Info: "Σκουπίδια και μπετά μέσα σε οικόπεδο πρέπει να απομακρυνθούν. Η εργασία απαιτεί φορτηγό", 25 | } 26 | 27 | var TwoDaysAgoRequestAssigned = RequestData{ 28 | Title: "Κούρεμα γκαζόν και κλαδιών", 29 | Postcode: "14660", 30 | Info: "Κούρεμα γκαζόν 50 τ.μ. και κλάδεμα 2 πεύκων ύψους 3 μέτρων", 31 | } 32 | -------------------------------------------------------------------------------- /clients/frontend/src/components/ProfileImageBadge.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler } from "react"; 2 | 3 | export const ProfileImageBadge = ({ 4 | src, 5 | badgeNumber, 6 | onBadgeClick, 7 | }: { 8 | src: string; 9 | badgeNumber: number; 10 | onBadgeClick: MouseEventHandler; 11 | }) => { 12 | return ( 13 |
14 | 15 | {badgeNumber >= 0 && ( 16 |
30 | {badgeNumber} 31 |
32 | )} 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /services/auction-service/internal/routes/events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | bidController "github.com/PanGan21/auction-service/internal/routes/events/bid" 5 | requestController "github.com/PanGan21/auction-service/internal/routes/events/request" 6 | "github.com/PanGan21/auction-service/internal/service" 7 | "github.com/PanGan21/pkg/logger" 8 | "github.com/PanGan21/pkg/messaging" 9 | ) 10 | 11 | const ( 12 | BID_CREATED_TOPIC = "bid-created" 13 | REQUEST_APPROVED_TOPIC = "request-approved" 14 | ) 15 | 16 | func NewEventsClient(subscriber messaging.Subscriber, l logger.Interface, bidService service.BidService, auctionService service.AuctionService) { 17 | bidController := bidController.NewBidController(l, bidService) 18 | requestController := requestController.NewRequestController(l, auctionService) 19 | 20 | go subscriber.Subscribe(BID_CREATED_TOPIC, bidController.Create) 21 | go subscriber.Subscribe(REQUEST_APPROVED_TOPIC, requestController.Create) 22 | } 23 | -------------------------------------------------------------------------------- /clients/frontend/src/types/auction.ts: -------------------------------------------------------------------------------- 1 | export interface Auction { 2 | Id: string; 3 | Title: string; 4 | CreatorId: string; 5 | Postcode: string; 6 | Info: string; 7 | Deadline: number; 8 | Status: string; 9 | RejectionReason: string; 10 | WinningBidId: string; 11 | WinnerId: string; 12 | WinningAmount: string; 13 | } 14 | 15 | export interface ExtendedAuction extends Auction { 16 | BidsCount: number; 17 | } 18 | 19 | export interface FormattedAuction { 20 | Id: Auction["Id"]; 21 | Title: Auction["Title"]; 22 | CreatorId: Auction["CreatorId"]; 23 | Postcode: Auction["Postcode"]; 24 | Info: Auction["Info"]; 25 | Deadline: string; 26 | Status: Auction["Status"]; 27 | RejectionReason: Auction["RejectionReason"]; 28 | WinnerId: Auction["WinnerId"]; 29 | WinningBidId: Auction["WinningBidId"]; 30 | WinningAmount: Auction["WinningAmount"]; 31 | } 32 | 33 | export interface ExtendedFormattedAuction extends FormattedAuction { 34 | BidsCount: number; 35 | } 36 | -------------------------------------------------------------------------------- /services/bidding-service/internal/routes/events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | auctionController "github.com/PanGan21/bidding-service/internal/routes/events/auction" 5 | requestController "github.com/PanGan21/bidding-service/internal/routes/events/request" 6 | "github.com/PanGan21/bidding-service/internal/service" 7 | "github.com/PanGan21/pkg/logger" 8 | "github.com/PanGan21/pkg/messaging" 9 | ) 10 | 11 | const ( 12 | REQUEST_APPROVED_TOPIC = "request-approved" 13 | AUCTION_UPDATED_TOPIC = "auction-updated" 14 | ) 15 | 16 | func NewEventsClient(subscriber messaging.Subscriber, l logger.Interface, auctionService service.AuctionService) { 17 | auctionController := auctionController.NewAuctionController(l, auctionService) 18 | requestController := requestController.NewRequestController(l, auctionService) 19 | 20 | go subscriber.Subscribe(REQUEST_APPROVED_TOPIC, requestController.Create) 21 | go subscriber.Subscribe(AUCTION_UPDATED_TOPIC, auctionController.Update) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/auth/types.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/PanGan21/pkg/entity" 9 | ) 10 | 11 | type AuthTokenData struct { 12 | Service string 13 | Route string 14 | SessionId string 15 | User entity.PublicUser 16 | Roles []string 17 | } 18 | 19 | type InternalPath struct { 20 | Service string 21 | Route string 22 | } 23 | 24 | func SplitPath(path string) (InternalPath, error) { 25 | var internalPath = InternalPath{Service: "", Route: "/"} 26 | 27 | path = strings.Split(path, "?")[0] 28 | regex, err := regexp.Compile("/([^/]*)(.*)") 29 | if err != nil { 30 | return internalPath, err 31 | } 32 | 33 | match := regex.FindStringSubmatch(path) 34 | 35 | if len(match) > 0 { 36 | internalPath.Service = match[1] 37 | } 38 | 39 | if len(match) > 1 { 40 | internalPath.Route = match[2] 41 | } 42 | 43 | if internalPath.Service == "" { 44 | return internalPath, errors.New("missing service") 45 | } 46 | 47 | return internalPath, nil 48 | } 49 | -------------------------------------------------------------------------------- /services/auction-service/internal/routes/events/bid/controller.go: -------------------------------------------------------------------------------- 1 | package bid 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/PanGan21/auction-service/internal/service" 8 | "github.com/PanGan21/pkg/entity" 9 | "github.com/PanGan21/pkg/logger" 10 | "github.com/PanGan21/pkg/messaging" 11 | ) 12 | 13 | type BidController interface { 14 | Create(msg messaging.Message) error 15 | } 16 | 17 | type bidController struct { 18 | logger logger.Interface 19 | bidService service.BidService 20 | } 21 | 22 | func NewBidController(logger logger.Interface, bidServ service.BidService) BidController { 23 | return &bidController{ 24 | logger: logger, 25 | bidService: bidServ, 26 | } 27 | } 28 | 29 | func (controller *bidController) Create(msg messaging.Message) error { 30 | bid, err := entity.IsBidType(msg.Payload) 31 | if err != nil { 32 | controller.logger.Error(err) 33 | log.Fatal(err) 34 | } 35 | 36 | err = controller.bidService.Create(context.Background(), bid) 37 | if err != nil { 38 | controller.logger.Error(err) 39 | log.Fatal(err) 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Panagiotis Ganelis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/entity/bid.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "errors" 4 | 5 | type Bid struct { 6 | Id int `json:"Id" db:"Id"` 7 | Amount float64 `json:"Amount" db:"Amount"` 8 | CreatorId string `json:"CreatorId" db:"CreatorId"` 9 | AuctionId int `json:"AuctionId" db:"AuctionId"` 10 | } 11 | 12 | var ErrIncorrectBidType = errors.New("incorrect bid type") 13 | 14 | func IsBidType(unknown interface{}) (Bid, error) { 15 | var bid Bid 16 | 17 | unknownMap, ok := unknown.(map[string]interface{}) 18 | if !ok { 19 | return bid, ErrIncorrectBidType 20 | } 21 | 22 | floatId, ok := unknownMap["Id"].(float64) 23 | if !ok { 24 | return bid, ErrIncorrectBidType 25 | } 26 | 27 | bid.Id = int(floatId) 28 | 29 | bid.Amount, ok = unknownMap["Amount"].(float64) 30 | if !ok { 31 | return bid, ErrIncorrectBidType 32 | } 33 | 34 | bid.CreatorId, ok = unknownMap["CreatorId"].(string) 35 | if !ok { 36 | return bid, ErrIncorrectBidType 37 | } 38 | 39 | floatAuctionId, ok := unknownMap["AuctionId"].(float64) 40 | if !ok { 41 | return bid, ErrIncorrectBidType 42 | } 43 | 44 | bid.AuctionId = int(floatAuctionId) 45 | 46 | return bid, nil 47 | } 48 | -------------------------------------------------------------------------------- /services/request-service/internal/repository/request/repository.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/PanGan21/pkg/entity" 7 | "github.com/PanGan21/pkg/pagination" 8 | ) 9 | 10 | type RequestRepository interface { 11 | Create(ctx context.Context, creatorId, info, postcode, title string, status entity.RequestStatus, rejectionReason string) (int, error) 12 | FindOneById(ctx context.Context, id int) (entity.Request, error) 13 | UpdateStatusAndRejectionReasonById(ctx context.Context, id int, status entity.RequestStatus, rejectionReason string) (entity.Request, error) 14 | GetAllByStatus(ctx context.Context, status entity.RequestStatus, pagination *pagination.Pagination) (*[]entity.Request, error) 15 | CountAllByStatus(ctx context.Context, status entity.RequestStatus) (int, error) 16 | GetManyByStatusByUserId(ctx context.Context, status entity.RequestStatus, userId string, pagination *pagination.Pagination) (*[]entity.Request, error) 17 | CountManyByStatusByUserId(ctx context.Context, status entity.RequestStatus, userId string) (int, error) 18 | UpdateStatusById(ctx context.Context, id int, status entity.RequestStatus) (entity.Request, error) 19 | } 20 | -------------------------------------------------------------------------------- /demo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/demo 2 | 3 | go 1.20 4 | 5 | replace ( 6 | github.com/PanGan21/pkg/entity => ../pkg/entity 7 | github.com/PanGan21/pkg/postgres => ../pkg/postgres 8 | ) 9 | 10 | require ( 11 | github.com/PanGan21/pkg/entity v0.0.0-00010101000000-000000000000 12 | github.com/PanGan21/pkg/postgres v0.0.0-00010101000000-000000000000 13 | ) 14 | 15 | require ( 16 | github.com/Masterminds/squirrel v1.5.3 // indirect 17 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 18 | github.com/jackc/pgconn v1.13.0 // indirect 19 | github.com/jackc/pgio v1.0.0 // indirect 20 | github.com/jackc/pgpassfile v1.0.0 // indirect 21 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 22 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 23 | github.com/jackc/pgtype v1.12.0 // indirect 24 | github.com/jackc/pgx/v4 v4.17.2 // indirect 25 | github.com/jackc/puddle v1.3.0 // indirect 26 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 27 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 28 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 29 | golang.org/x/text v0.3.7 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /pkg/pagination/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/pkg/pagination 2 | 3 | go 1.20 4 | 5 | require github.com/gin-gonic/gin v1.8.1 6 | 7 | require ( 8 | github.com/gin-contrib/sse v0.1.0 // indirect 9 | github.com/go-playground/locales v0.14.0 // indirect 10 | github.com/go-playground/universal-translator v0.18.0 // indirect 11 | github.com/go-playground/validator/v10 v10.10.0 // indirect 12 | github.com/goccy/go-json v0.9.7 // indirect 13 | github.com/json-iterator/go v1.1.12 // indirect 14 | github.com/leodido/go-urn v1.2.1 // indirect 15 | github.com/mattn/go-isatty v0.0.14 // indirect 16 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 17 | github.com/modern-go/reflect2 v1.0.2 // indirect 18 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 19 | github.com/ugorji/go/codec v1.2.7 // indirect 20 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect 21 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect 22 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect 23 | golang.org/x/text v0.3.6 // indirect 24 | google.golang.org/protobuf v1.28.0 // indirect 25 | gopkg.in/yaml.v2 v2.4.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /services/user-service/migrations/000002_create_users_data.up.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO users (Username, Email, Phone, PasswordHash, Roles) 2 | VALUES ('SuperAdmin', 'superadmin@email.com', '+306900000000', 'c88e9c67041a74e0357befdff93f87dde0904214', ARRAY ['ADMIN']); 3 | 4 | INSERT INTO users (Username, Email, Phone, PasswordHash, Roles) 5 | VALUES ('Resident1', 'resident1@email.com', '+306900000001', 'c88e9c67041a74e0357befdff93f87dde0904214', ARRAY ['RESIDENT']); 6 | 7 | INSERT INTO users (Username, Email, Phone, PasswordHash, Roles) 8 | VALUES ('Resident2', 'resident2@email.com', '+306900000002', 'c88e9c67041a74e0357befdff93f87dde0904214', ARRAY ['RESIDENT']); 9 | 10 | INSERT INTO users (Username, Email, Phone, PasswordHash, Roles) 11 | VALUES ('Bidder1','bidder1@email.com', '+306900000003', 'c88e9c67041a74e0357befdff93f87dde0904214', ARRAY ['BIDDER']); 12 | 13 | INSERT INTO users (Username, Email, Phone, PasswordHash, Roles) 14 | VALUES ('Bidder2','bidder2@email.com', '+306900000004', 'c88e9c67041a74e0357befdff93f87dde0904214', ARRAY ['BIDDER']); 15 | 16 | INSERT INTO users (Username, Email, Phone, PasswordHash, Roles) 17 | VALUES ('Bidder3','bidder3@email.com', '+306900000005', 'c88e9c67041a74e0357befdff93f87dde0904214', ARRAY ['BIDDER']); -------------------------------------------------------------------------------- /pkg/logger/go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 3 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 4 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 5 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 6 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 7 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 8 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 9 | github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= 10 | github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= 11 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 12 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= 13 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 14 | -------------------------------------------------------------------------------- /api-gateway/api_proxy.conf: -------------------------------------------------------------------------------- 1 | if ($request_method = 'OPTIONS') { 2 | add_header 'Access-Control-Allow-Origin' $http_origin; 3 | add_header 'Access-Control-Allow-Credentials' 'true' always; 4 | add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 5 | add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; 6 | add_header 'Access-Control-Max-Age' 1728000; 7 | add_header 'Content-Type' 'text/plain charset=UTF-8'; 8 | add_header 'Content-Length' 0; 9 | return 204; 10 | } 11 | if ($request_method = 'POST') { 12 | add_header 'Access-Control-Allow-Credentials' 'true' always; 13 | add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 14 | add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; 15 | } 16 | if ($request_method = 'GET') { 17 | add_header 'Access-Control-Allow-Credentials' 'true' always; 18 | add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 19 | add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; 20 | } -------------------------------------------------------------------------------- /pkg/auth/gin_middleware.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/PanGan21/pkg/utils" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func VerifyJWT(authService AuthService) gin.HandlerFunc { 11 | fn := func(c *gin.Context) { 12 | jwt := c.Request.Header.Get("x-internal-jwt") 13 | if jwt == "" { 14 | c.JSON(http.StatusUnauthorized, gin.H{"error": "no token found"}) 15 | return 16 | } 17 | 18 | authTokenData, err := authService.VerifyJWT(jwt, c.Request.URL.Path) 19 | if err != nil { 20 | c.JSON(http.StatusUnauthorized, gin.H{"error": "jwt not verified"}) 21 | return 22 | } 23 | 24 | c.Set("url", authTokenData.Route) 25 | c.Set("jwt", jwt) 26 | c.Set("user", authTokenData.User) 27 | c.Set("roles", authTokenData.Roles) 28 | 29 | c.Next() 30 | } 31 | 32 | return gin.HandlerFunc(fn) 33 | } 34 | 35 | func AuthorizeEndpoint(allowedRoles ...string) gin.HandlerFunc { 36 | fn := func(c *gin.Context) { 37 | requestRoles := c.Copy().Value("roles").([]string) 38 | 39 | if !utils.Subslice(allowedRoles, requestRoles) { 40 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "incorrect permissions"}) 41 | return 42 | } 43 | 44 | c.Next() 45 | } 46 | 47 | return gin.HandlerFunc(fn) 48 | } 49 | -------------------------------------------------------------------------------- /demo/auction.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/http/cookiejar" 9 | 10 | "github.com/PanGan21/pkg/entity" 11 | ) 12 | 13 | var auctionApi = getBasePath("auction") 14 | 15 | func updateAuctionWinner(session string, auctionId string) error { 16 | updateAuctionWinnerPath := auctionApi + "/update/winner?auctionId=" + auctionId 17 | 18 | jar, err := cookiejar.New(nil) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | client := http.Client{ 24 | Jar: jar, 25 | } 26 | 27 | cookie := &http.Cookie{ 28 | Name: "s.id", 29 | Value: session, 30 | } 31 | 32 | req, err := http.NewRequest("POST", updateAuctionWinnerPath, nil) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | req.AddCookie(cookie) 38 | res, err := client.Do(req) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | if res.StatusCode != 200 { 44 | return fmt.Errorf("auction winner update failed") 45 | } 46 | 47 | resBody, err := io.ReadAll(res.Body) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | var auction entity.Auction 53 | 54 | err = json.Unmarshal(resBody, &auction) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | fmt.Println("Auction winner updated!", auction) 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /clients/frontend/src/services/auth.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { API_URL } from "../constants"; 3 | import { User } from "../types/user"; 4 | 5 | export const login = async (username: string, password: string) => { 6 | return axios.post( 7 | API_URL + "/user/login", 8 | { 9 | username, 10 | password, 11 | }, 12 | { withCredentials: true } 13 | ); 14 | }; 15 | 16 | export const getLoggedInUserDetails = async () => { 17 | return axios.get(API_URL + "/user/", { withCredentials: true }); 18 | }; 19 | 20 | export const logout = async () => { 21 | localStorage.removeItem("user"); 22 | clearCookies(); 23 | await axios.post(API_URL + "/user/logout", {}, { withCredentials: true }); 24 | }; 25 | 26 | export const getCurrentUser = (): User | undefined => { 27 | const userStr = localStorage.getItem("user"); 28 | if (userStr) { 29 | return JSON.parse(userStr); 30 | } 31 | }; 32 | 33 | export const getUserDetailsById = async (id: string) => { 34 | return axios.get(API_URL + "/user/details?userId=" + id, { 35 | withCredentials: true, 36 | }); 37 | }; 38 | 39 | const clearCookies = () => { 40 | document.cookie.split(";").forEach((c) => { 41 | document.cookie = c 42 | .replace(/^ +/, "") 43 | .replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /demo/bid.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/http/cookiejar" 10 | 11 | "github.com/PanGan21/demo/data" 12 | "github.com/PanGan21/pkg/entity" 13 | ) 14 | 15 | var biddingApi = getBasePath("bidding") 16 | 17 | func createBid(session string, bidData data.BidData) (entity.Bid, error) { 18 | createBidPath := biddingApi + "/" 19 | 20 | var bid entity.Bid 21 | 22 | body, err := json.Marshal(bidData) 23 | if err != nil { 24 | return bid, err 25 | } 26 | 27 | jar, err := cookiejar.New(nil) 28 | if err != nil { 29 | return bid, err 30 | } 31 | 32 | client := http.Client{ 33 | Jar: jar, 34 | } 35 | 36 | cookie := &http.Cookie{ 37 | Name: "s.id", 38 | Value: session, 39 | } 40 | 41 | req, err := http.NewRequest("POST", createBidPath, bytes.NewBuffer(body)) 42 | if err != nil { 43 | return bid, err 44 | } 45 | 46 | req.AddCookie(cookie) 47 | res, err := client.Do(req) 48 | if err != nil { 49 | return bid, err 50 | } 51 | 52 | if res.StatusCode != 200 { 53 | return bid, fmt.Errorf("bid creation failed") 54 | } 55 | 56 | resBody, err := io.ReadAll(res.Body) 57 | if err != nil { 58 | return bid, err 59 | } 60 | 61 | err = json.Unmarshal(resBody, &bid) 62 | if err != nil { 63 | return bid, err 64 | } 65 | 66 | fmt.Println("Bid created!", bid) 67 | 68 | return bid, nil 69 | } 70 | -------------------------------------------------------------------------------- /demo/user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/http/cookiejar" 9 | 10 | "github.com/PanGan21/demo/data" 11 | ) 12 | 13 | var userApi = getBasePath("user") 14 | 15 | func login(userData data.UserData) (string, error) { 16 | loginPath := userApi + "/login" 17 | var session string 18 | 19 | body, err := json.Marshal(userData) 20 | if err != nil { 21 | return session, err 22 | } 23 | 24 | res, err := http.Post(loginPath, "application/json", bytes.NewBuffer(body)) 25 | if err != nil { 26 | return session, err 27 | } 28 | 29 | if res.StatusCode != 200 { 30 | return session, fmt.Errorf("login failed") 31 | } 32 | 33 | for _, c := range res.Cookies() { 34 | if c.Name == "s.id" { 35 | session = c.Value 36 | } 37 | } 38 | 39 | return session, nil 40 | } 41 | 42 | func logout(session string) error { 43 | logoutPath := userApi + "/logout" 44 | 45 | jar, err := cookiejar.New(nil) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | client := http.Client{ 51 | Jar: jar, 52 | } 53 | 54 | cookie := &http.Cookie{ 55 | Name: "s.id", 56 | Value: session, 57 | } 58 | 59 | req, err := http.NewRequest("POST", logoutPath, nil) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | req.AddCookie(cookie) 65 | res, err := client.Do(req) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | if res.StatusCode != 200 { 71 | return fmt.Errorf("logout failed") 72 | } 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /pkg/httpserver/server.go: -------------------------------------------------------------------------------- 1 | // Package httpserver implements HTTP server. 2 | package httpserver 3 | 4 | import ( 5 | "context" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | const ( 11 | _defaultReadTimeout = 5 * time.Second 12 | _defaultWriteTimeout = 5 * time.Second 13 | _defaultAddr = ":80" 14 | _defaultShutdownTimeout = 3 * time.Second 15 | ) 16 | 17 | // Server -. 18 | type Server struct { 19 | server *http.Server 20 | notify chan error 21 | shutdownTimeout time.Duration 22 | } 23 | 24 | // New -. 25 | func New(handler http.Handler, opts ...Option) *Server { 26 | httpServer := &http.Server{ 27 | Handler: handler, 28 | ReadTimeout: _defaultReadTimeout, 29 | WriteTimeout: _defaultWriteTimeout, 30 | Addr: _defaultAddr, 31 | } 32 | 33 | s := &Server{ 34 | server: httpServer, 35 | notify: make(chan error, 1), 36 | shutdownTimeout: _defaultShutdownTimeout, 37 | } 38 | 39 | // Custom options 40 | for _, opt := range opts { 41 | opt(s) 42 | } 43 | 44 | s.start() 45 | 46 | return s 47 | } 48 | 49 | func (s *Server) start() { 50 | go func() { 51 | s.notify <- s.server.ListenAndServe() 52 | close(s.notify) 53 | }() 54 | } 55 | 56 | // Notify -. 57 | func (s *Server) Notify() <-chan error { 58 | return s.notify 59 | } 60 | 61 | // Shutdown -. 62 | func (s *Server) Shutdown() error { 63 | ctx, cancel := context.WithTimeout(context.Background(), s.shutdownTimeout) 64 | defer cancel() 65 | 66 | return s.server.Shutdown(ctx) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/auth/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/pkg/auth 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/PanGan21/pkg/entity v0.0.0-00010101000000-000000000000 7 | github.com/PanGan21/pkg/utils v0.0.0-00010101000000-000000000000 8 | github.com/gin-gonic/gin v1.8.1 9 | github.com/golang-jwt/jwt/v4 v4.4.2 10 | github.com/mitchellh/mapstructure v1.5.0 11 | ) 12 | 13 | require ( 14 | github.com/gin-contrib/sse v0.1.0 // indirect 15 | github.com/go-playground/locales v0.14.0 // indirect 16 | github.com/go-playground/universal-translator v0.18.0 // indirect 17 | github.com/go-playground/validator/v10 v10.10.0 // indirect 18 | github.com/goccy/go-json v0.9.7 // indirect 19 | github.com/json-iterator/go v1.1.12 // indirect 20 | github.com/leodido/go-urn v1.2.1 // indirect 21 | github.com/mattn/go-isatty v0.0.14 // indirect 22 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 23 | github.com/modern-go/reflect2 v1.0.2 // indirect 24 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 25 | github.com/ugorji/go/codec v1.2.7 // indirect 26 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect 27 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect 28 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect 29 | golang.org/x/text v0.3.6 // indirect 30 | google.golang.org/protobuf v1.28.0 // indirect 31 | gopkg.in/yaml.v2 v2.4.0 // indirect 32 | ) 33 | 34 | replace ( 35 | github.com/PanGan21/pkg/entity => ../entity 36 | github.com/PanGan21/pkg/utils => ../utils 37 | ) 38 | -------------------------------------------------------------------------------- /services/auction-service/internal/routes/events/request/controller.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/PanGan21/auction-service/internal/service" 8 | "github.com/PanGan21/pkg/entity" 9 | "github.com/PanGan21/pkg/logger" 10 | "github.com/PanGan21/pkg/messaging" 11 | ) 12 | 13 | type RequestController interface { 14 | Create(msg messaging.Message) error 15 | } 16 | 17 | type requestController struct { 18 | logger logger.Interface 19 | auctionService service.AuctionService 20 | } 21 | 22 | func NewRequestController(logger logger.Interface, auctionServ service.AuctionService) RequestController { 23 | return &requestController{ 24 | logger: logger, 25 | auctionService: auctionServ, 26 | } 27 | } 28 | 29 | func (controller *requestController) Create(msg messaging.Message) error { 30 | request, err := entity.IsRequestType(msg.Payload) 31 | if err != nil { 32 | controller.logger.Error(err) 33 | log.Fatal(err) 34 | } 35 | 36 | newAuction := entity.Auction{ 37 | Id: request.Id, 38 | Title: request.Title, 39 | Postcode: request.Postcode, 40 | Info: request.Info, 41 | CreatorId: request.CreatorId, 42 | Deadline: msg.Timestamp, 43 | Status: entity.Open, 44 | WinningBidId: "", 45 | WinnerId: "", 46 | WinningAmount: 0.0, 47 | } 48 | 49 | _, err = controller.auctionService.Create(context.Background(), newAuction) 50 | if err != nil { 51 | controller.logger.Error(err) 52 | log.Fatal(err) 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /services/bidding-service/internal/routes/events/request/controller.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/PanGan21/bidding-service/internal/service" 8 | "github.com/PanGan21/pkg/entity" 9 | "github.com/PanGan21/pkg/logger" 10 | "github.com/PanGan21/pkg/messaging" 11 | ) 12 | 13 | type RequestController interface { 14 | Create(msg messaging.Message) error 15 | } 16 | 17 | type requestController struct { 18 | logger logger.Interface 19 | auctionService service.AuctionService 20 | } 21 | 22 | func NewRequestController(logger logger.Interface, auctionServ service.AuctionService) RequestController { 23 | return &requestController{ 24 | logger: logger, 25 | auctionService: auctionServ, 26 | } 27 | } 28 | 29 | func (controller *requestController) Create(msg messaging.Message) error { 30 | request, err := entity.IsRequestType(msg.Payload) 31 | if err != nil { 32 | controller.logger.Error(err) 33 | log.Fatal(err) 34 | } 35 | 36 | newAuction := entity.Auction{ 37 | Id: request.Id, 38 | Title: request.Title, 39 | Postcode: request.Postcode, 40 | Info: request.Info, 41 | CreatorId: request.CreatorId, 42 | Deadline: msg.Timestamp, 43 | Status: entity.Open, 44 | WinningBidId: "", 45 | WinnerId: "", 46 | WinningAmount: 0.0, 47 | } 48 | 49 | err = controller.auctionService.Create(context.Background(), newAuction) 50 | if err != nil { 51 | controller.logger.Error(err) 52 | log.Fatal(err) 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /services/bidding-service/internal/routes/http/routes.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | bidController "github.com/PanGan21/bidding-service/internal/routes/http/bid" 8 | "github.com/PanGan21/bidding-service/internal/service" 9 | "github.com/PanGan21/pkg/auth" 10 | "github.com/PanGan21/pkg/logger" 11 | "github.com/gin-contrib/cors" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func NewRouter(handler *gin.Engine, l logger.Interface, corsOrigins []string, authService auth.AuthService, bidService service.BidService, auctionService service.AuctionService) { 16 | bidController := bidController.NewBidController(l, bidService, auctionService) 17 | // Options 18 | handler.Use(gin.Recovery()) 19 | 20 | // Cors 21 | handler.Use(cors.New(cors.Config{ 22 | AllowOrigins: corsOrigins, 23 | AllowMethods: []string{"POST", "GET", "OPTIONS"}, 24 | AllowHeaders: []string{"DNT", "X-CustomHeader", "Keep-Alive", "User-Agent", "X-Requested-With", "If-Modified-Since", "Cache-Control", "Content-Type"}, 25 | MaxAge: 12 * time.Hour, 26 | })) 27 | 28 | // K8s probe 29 | handler.GET("/healthz", func(c *gin.Context) { c.Status(http.StatusOK) }) 30 | 31 | // JWT Middleware 32 | handler.Use(auth.VerifyJWT(authService)) 33 | 34 | // Routers 35 | var requiredRoles []string 36 | handler.POST("/", auth.AuthorizeEndpoint(requiredRoles...), bidController.Create) 37 | handler.GET("/", bidController.GetOneById) 38 | handler.GET("/auctionId/", bidController.GetManyByAuctionId) 39 | handler.GET("/count/own", bidController.CountOwn) 40 | handler.GET("/own", bidController.GetOwn) 41 | } 42 | -------------------------------------------------------------------------------- /clients/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "5.14.1", 7 | "@testing-library/react": "13.0.0", 8 | "@testing-library/user-event": "13.2.1", 9 | "@types/jest": "27.0.1", 10 | "@types/node": "16.7.13", 11 | "@types/react": "18.0.0", 12 | "@types/react-dom": "18.0.0", 13 | "@types/react-table": "^7.7.14", 14 | "axios": "1.2.2", 15 | "bootstrap": "5.2.3", 16 | "formik": "2.2.9", 17 | "react": "18.2.0", 18 | "react-bootstrap-icons": "^1.10.2", 19 | "react-dom": "18.2.0", 20 | "react-router-dom": "6.6.2", 21 | "react-scripts": "5.0.1", 22 | "react-table": "^7.8.0", 23 | "typescript": "4.4.2", 24 | "web-vitals": "2.1.0", 25 | "yup": "0.32.11" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject", 32 | "start-https": "REACT_APP_API_URL=https://localhost HTTPS=true SSL_CRT_FILE=../../ssl/cert.pem SSL_KEY_FILE=../../ssl/key.pem yarn start" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "prettier": "^2.8.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /services/bidding-service/internal/routes/events/auction/controller.go: -------------------------------------------------------------------------------- 1 | package auction 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/PanGan21/bidding-service/internal/service" 8 | "github.com/PanGan21/pkg/entity" 9 | "github.com/PanGan21/pkg/logger" 10 | "github.com/PanGan21/pkg/messaging" 11 | ) 12 | 13 | type AuctionController interface { 14 | Create(msg messaging.Message) error 15 | Update(msg messaging.Message) error 16 | } 17 | 18 | type auctionController struct { 19 | logger logger.Interface 20 | auctionService service.AuctionService 21 | } 22 | 23 | func NewAuctionController(logger logger.Interface, auctionServ service.AuctionService) AuctionController { 24 | return &auctionController{ 25 | logger: logger, 26 | auctionService: auctionServ, 27 | } 28 | } 29 | 30 | func (controller *auctionController) Create(msg messaging.Message) error { 31 | auction, err := entity.IsAuctionType(msg.Payload) 32 | if err != nil { 33 | controller.logger.Error(err) 34 | log.Fatal(err) 35 | } 36 | 37 | err = controller.auctionService.Create(context.Background(), auction) 38 | if err != nil { 39 | controller.logger.Error(err) 40 | log.Fatal(err) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func (controller *auctionController) Update(msg messaging.Message) error { 47 | auction, err := entity.IsAuctionType(msg.Payload) 48 | if err != nil { 49 | controller.logger.Error(err) 50 | log.Fatal(err) 51 | } 52 | 53 | err = controller.auctionService.UpdateOne(context.Background(), auction) 54 | if err != nil { 55 | controller.logger.Error(err) 56 | log.Fatal(err) 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /services/bidding-service/internal/service/auction.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | auctionRepo "github.com/PanGan21/bidding-service/internal/repository/auction" 8 | "github.com/PanGan21/pkg/entity" 9 | ) 10 | 11 | type AuctionService interface { 12 | Create(ctx context.Context, auction entity.Auction) error 13 | UpdateOne(ctx context.Context, auction entity.Auction) error 14 | IsOpenToBidByAuctionId(ctx context.Context, auctionId int) bool 15 | } 16 | 17 | type auctionService struct { 18 | auctionRepo auctionRepo.AuctionRepository 19 | } 20 | 21 | func NewAuctionService(rr auctionRepo.AuctionRepository) AuctionService { 22 | return &auctionService{auctionRepo: rr} 23 | } 24 | 25 | func (s *auctionService) Create(ctx context.Context, auction entity.Auction) error { 26 | err := s.auctionRepo.Create(ctx, auction) 27 | if err != nil { 28 | return fmt.Errorf("AuctionService - Create - s.auctionRepo.Create: %w", err) 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func (s *auctionService) UpdateOne(ctx context.Context, auction entity.Auction) error { 35 | err := s.auctionRepo.UpdateOne(ctx, auction) 36 | if err != nil { 37 | return fmt.Errorf("AuctionService - UpdateOne - s.auctionRepo.UpdateOne: %w", err) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (s *auctionService) IsOpenToBidByAuctionId(ctx context.Context, auctionId int) bool { 44 | auction, err := s.auctionRepo.FindOneById(ctx, auctionId) 45 | if err != nil { 46 | fmt.Println("AuctionService - IsOpenToBidByAuctionId - s.auctionRepo.FindOneById: %w", err) 47 | return false 48 | } 49 | 50 | return auction.Status == entity.Open 51 | } 52 | -------------------------------------------------------------------------------- /services/user-service/internal/routes/http/routes.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/PanGan21/pkg/auth" 8 | "github.com/PanGan21/pkg/logger" 9 | userController "github.com/PanGan21/user-service/internal/routes/http/user" 10 | "github.com/PanGan21/user-service/internal/service" 11 | 12 | "github.com/gin-contrib/cors" 13 | "github.com/gin-gonic/contrib/sessions" 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | func NewRouter(handler *gin.Engine, l logger.Interface, corsOrigins []string, store sessions.RedisStore, userService service.UserService, authService auth.AuthService) { 18 | userController := userController.NewUserController(l, userService, authService) 19 | // Options 20 | handler.Use(gin.Recovery()) 21 | 22 | // Cors 23 | handler.Use(cors.New(cors.Config{ 24 | AllowOrigins: corsOrigins, 25 | AllowMethods: []string{"POST", "GET", "OPTIONS"}, 26 | AllowHeaders: []string{"DNT", "X-CustomHeader", "Keep-Alive", "User-Agent", "X-Requested-With", "If-Modified-Since", "Cache-Control", "Content-Type"}, 27 | MaxAge: 12 * time.Hour, 28 | })) 29 | 30 | // K8s probe 31 | handler.GET("/healthz", func(c *gin.Context) { c.Status(http.StatusOK) }) 32 | 33 | // Session 34 | handler.Use(sessions.Sessions("s.id", store)) 35 | 36 | // Routers 37 | handler.GET("/", userController.GetLoggedInUserDetails) 38 | handler.POST("/login", userController.Login) 39 | handler.POST("/logout", userController.Logout) 40 | handler.POST("/register", userController.Register) 41 | handler.GET("/authenticate", userController.Authenticate) 42 | handler.GET("/details", userController.GetDetailsById) 43 | } 44 | -------------------------------------------------------------------------------- /services/auction-service/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ilyakaznacheev/cleanenv" 7 | ) 8 | 9 | type ( 10 | Config struct { 11 | App `yaml:"app"` 12 | HTTP `yaml:"http"` 13 | Log `yaml:"logger"` 14 | Postgres `yaml:"postgres"` 15 | Kafka `yaml:"kafka"` 16 | } 17 | 18 | App struct { 19 | Name string `env-required:"true" yaml:"name" env:"APP_NAME"` 20 | } 21 | 22 | // HTTP -. 23 | HTTP struct { 24 | Port string `env-required:"true" yaml:"port" env:"HTTP_PORT"` 25 | SessionSecret string `env-required:"true" yaml:"session_secret" env:"SESSION_SECRET"` 26 | AuthSecret string `env-required:"true" yaml:"auth_secret" env:"AUTH_SECRET"` 27 | CorsOrigins []string `env-required:"true" yaml:"cors_origins" env:"CORS_ORIGINS"` 28 | } 29 | 30 | // Log -. 31 | Log struct { 32 | Level string `env-required:"true" yaml:"log_level" env:"LOG_LEVEL"` 33 | } 34 | 35 | // Postgres -. 36 | Postgres struct { 37 | PoolMax int `env-required:"true" yaml:"pool_max" env:"PG_POOL_MAX"` 38 | URL string `env-required:"true" yaml:"url" env:"PG_URL"` 39 | } 40 | 41 | // Kafka -. 42 | Kafka struct { 43 | Retries int `env-required:"true" yaml:"retries" env:"KAFKA_RETRIES"` 44 | URL string `env-required:"true" yaml:"url" env:"KAFKA_URL"` 45 | } 46 | ) 47 | 48 | // NewConfig returns app config. 49 | func NewConfig() (*Config, error) { 50 | cfg := &Config{} 51 | 52 | err := cleanenv.ReadConfig("./config/config.yaml", cfg) 53 | if err != nil { 54 | return nil, fmt.Errorf("config error: %w", err) 55 | } 56 | 57 | err = cleanenv.ReadEnv(cfg) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return cfg, nil 63 | } 64 | -------------------------------------------------------------------------------- /services/bidding-service/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ilyakaznacheev/cleanenv" 7 | ) 8 | 9 | type ( 10 | Config struct { 11 | App `yaml:"app"` 12 | HTTP `yaml:"http"` 13 | Log `yaml:"logger"` 14 | Postgres `yaml:"postgres"` 15 | Kafka `yaml:"kafka"` 16 | } 17 | 18 | App struct { 19 | Name string `env-required:"true" yaml:"name" env:"APP_NAME"` 20 | } 21 | 22 | // HTTP -. 23 | HTTP struct { 24 | Port string `env-required:"true" yaml:"port" env:"HTTP_PORT"` 25 | SessionSecret string `env-required:"true" yaml:"session_secret" env:"SESSION_SECRET"` 26 | AuthSecret string `env-required:"true" yaml:"auth_secret" env:"AUTH_SECRET"` 27 | CorsOrigins []string `env-required:"true" yaml:"cors_origins" env:"CORS_ORIGINS"` 28 | } 29 | 30 | // Log -. 31 | Log struct { 32 | Level string `env-required:"true" yaml:"log_level" env:"LOG_LEVEL"` 33 | } 34 | 35 | // Postgres -. 36 | Postgres struct { 37 | PoolMax int `env-required:"true" yaml:"pool_max" env:"PG_POOL_MAX"` 38 | URL string `env-required:"true" yaml:"url" env:"PG_URL"` 39 | } 40 | 41 | // Kafka -. 42 | Kafka struct { 43 | Retries int `env-required:"true" yaml:"retries" env:"KAFKA_RETRIES"` 44 | URL string `env-required:"true" yaml:"url" env:"KAFKA_URL"` 45 | } 46 | ) 47 | 48 | // NewConfig returns app config. 49 | func NewConfig() (*Config, error) { 50 | cfg := &Config{} 51 | 52 | err := cleanenv.ReadConfig("./config/config.yaml", cfg) 53 | if err != nil { 54 | return nil, fmt.Errorf("config error: %w", err) 55 | } 56 | 57 | err = cleanenv.ReadEnv(cfg) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return cfg, nil 63 | } 64 | -------------------------------------------------------------------------------- /services/request-service/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ilyakaznacheev/cleanenv" 7 | ) 8 | 9 | type ( 10 | Config struct { 11 | App `yaml:"app"` 12 | HTTP `yaml:"http"` 13 | Log `yaml:"logger"` 14 | Postgres `yaml:"postgres"` 15 | Kafka `yaml:"kafka"` 16 | } 17 | 18 | App struct { 19 | Name string `env-required:"true" yaml:"name" env:"APP_NAME"` 20 | } 21 | 22 | // HTTP -. 23 | HTTP struct { 24 | Port string `env-required:"true" yaml:"port" env:"HTTP_PORT"` 25 | SessionSecret string `env-required:"true" yaml:"session_secret" env:"SESSION_SECRET"` 26 | AuthSecret string `env-required:"true" yaml:"auth_secret" env:"AUTH_SECRET"` 27 | CorsOrigins []string `env-required:"true" yaml:"cors_origins" env:"CORS_ORIGINS"` 28 | } 29 | 30 | // Log -. 31 | Log struct { 32 | Level string `env-required:"true" yaml:"log_level" env:"LOG_LEVEL"` 33 | } 34 | 35 | // Postgres -. 36 | Postgres struct { 37 | PoolMax int `env-required:"true" yaml:"pool_max" env:"PG_POOL_MAX"` 38 | URL string `env-required:"true" yaml:"url" env:"PG_URL"` 39 | } 40 | 41 | // Kafka -. 42 | Kafka struct { 43 | Retries int `env-required:"true" yaml:"retries" env:"KAFKA_RETRIES"` 44 | URL string `env-required:"true" yaml:"url" env:"KAFKA_URL"` 45 | } 46 | ) 47 | 48 | // NewConfig returns app config. 49 | func NewConfig() (*Config, error) { 50 | cfg := &Config{} 51 | 52 | err := cleanenv.ReadConfig("./config/config.yaml", cfg) 53 | if err != nil { 54 | return nil, fmt.Errorf("config error: %w", err) 55 | } 56 | 57 | err = cleanenv.ReadEnv(cfg) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return cfg, nil 63 | } 64 | -------------------------------------------------------------------------------- /clients/frontend/src/components/AssignedAuction.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | import bid from "../assets/bid.png"; 4 | import { getUserDetailsById } from "../services/auth"; 5 | import { Auction } from "../types/auction"; 6 | import { UserDetails } from "../types/user"; 7 | 8 | type Props = {}; 9 | 10 | export const AssignedAuction: React.FC = () => { 11 | const { state }: { state: Auction } = useLocation(); 12 | const [winnerDetails, setWinnerDetails] = useState( 13 | undefined 14 | ); 15 | 16 | useEffect(() => { 17 | if (state.WinnerId !== "") { 18 | getUserDetailsById(state.WinnerId).then((response) => { 19 | if (response.data) { 20 | setWinnerDetails(response.data); 21 | } 22 | }); 23 | } 24 | }, [state.WinnerId]); 25 | 26 | return ( 27 |
28 |
29 | profile-img 30 |

Winning bid

31 |
32 | Auction Id: {state.Id} 33 |
34 |
35 | Bidder Id: {state.WinnerId || "Pending"} 36 |
37 |
38 | Bidder Username: {winnerDetails?.Username || "Pending"} 39 |
40 |
41 | Bidder Email: {winnerDetails?.Email || "Pending"} 42 |
43 |
44 | Bidder Phone: {winnerDetails?.Phone || "Pending"} 45 |
46 |
47 | Amount (€): 48 | {state.WinningAmount || "Pending"} 49 |
50 |
51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /pkg/messaging/publisher.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | kafka "github.com/segmentio/kafka-go" 11 | ) 12 | 13 | type Publisher interface { 14 | Publish(topic string, message Message) error 15 | Close() error 16 | } 17 | 18 | type kafkaPublisher struct { 19 | publisher *kafka.Writer 20 | retries int 21 | } 22 | 23 | func NewPublisher(url string, retries int) Publisher { 24 | w := &kafka.Writer{ 25 | Addr: kafka.TCP(url), 26 | AllowAutoTopicCreation: false, // topics defined in environment variables in order to specify the partitions 27 | } 28 | 29 | return &kafkaPublisher{ 30 | publisher: w, 31 | retries: retries, 32 | } 33 | } 34 | 35 | func (k kafkaPublisher) Publish(topic string, msg Message) error { 36 | msgJson, err := json.Marshal(msg) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | const retries = 3 42 | for i := 0; i < retries; i++ { 43 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 44 | defer cancel() 45 | 46 | err = k.publisher.WriteMessages(ctx, 47 | kafka.Message{Topic: topic, Value: msgJson}) 48 | if errors.Is(err, kafka.LeaderNotAvailable) || errors.Is(err, context.DeadlineExceeded) { 49 | time.Sleep(time.Millisecond * 250) 50 | continue 51 | } 52 | 53 | if err == nil { 54 | break 55 | } 56 | 57 | if err != nil { 58 | fmt.Printf("unexpected error %v\n", err) 59 | return err 60 | } 61 | } 62 | 63 | if err != nil { 64 | fmt.Println("failed to write messages:", err) 65 | return err 66 | } 67 | 68 | fmt.Printf("PUBLISHER: Topic: %s - Payload: %v\n", topic, msg.Payload) 69 | 70 | return nil 71 | } 72 | 73 | func (k kafkaPublisher) Close() error { 74 | return k.publisher.Close() 75 | } 76 | -------------------------------------------------------------------------------- /pkg/postgres/postgres.go: -------------------------------------------------------------------------------- 1 | // Package postgres implements postgres connection. 2 | package postgres 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | "github.com/Masterminds/squirrel" 11 | "github.com/jackc/pgx/v4/pgxpool" 12 | ) 13 | 14 | const ( 15 | _defaultMaxPoolSize = 1 16 | _defaultConnAttempts = 10 17 | _defaultConnTimeout = time.Second 18 | ) 19 | 20 | // Postgres -. 21 | type Postgres struct { 22 | maxPoolSize int 23 | connAttempts int 24 | connTimeout time.Duration 25 | 26 | Builder squirrel.StatementBuilderType 27 | Pool *pgxpool.Pool 28 | } 29 | 30 | // New -. 31 | func New(url string, opts ...Option) (*Postgres, error) { 32 | pg := &Postgres{ 33 | maxPoolSize: _defaultMaxPoolSize, 34 | connAttempts: _defaultConnAttempts, 35 | connTimeout: _defaultConnTimeout, 36 | } 37 | 38 | // Custom options 39 | for _, opt := range opts { 40 | opt(pg) 41 | } 42 | 43 | pg.Builder = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) 44 | 45 | poolConfig, err := pgxpool.ParseConfig(url) 46 | if err != nil { 47 | return nil, fmt.Errorf("postgres - NewPostgres - pgxpool.ParseConfig: %w", err) 48 | } 49 | 50 | poolConfig.MaxConns = int32(pg.maxPoolSize) 51 | 52 | for pg.connAttempts > 0 { 53 | pg.Pool, err = pgxpool.ConnectConfig(context.Background(), poolConfig) 54 | 55 | if err == nil { 56 | break 57 | } 58 | 59 | log.Printf("Postgres is trying to connect, attempts left: %d", pg.connAttempts) 60 | 61 | time.Sleep(pg.connTimeout) 62 | 63 | pg.connAttempts-- 64 | } 65 | 66 | if err != nil { 67 | return nil, fmt.Errorf("postgres - NewPostgres - connAttempts == 0: %w", err) 68 | } 69 | 70 | return pg, nil 71 | } 72 | 73 | // Close -. 74 | func (p *Postgres) Close() { 75 | if p.Pool != nil { 76 | p.Pool.Close() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /services/request-service/internal/routes/http/routes.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/PanGan21/pkg/auth" 8 | "github.com/PanGan21/pkg/logger" 9 | requestController "github.com/PanGan21/request-service/internal/routes/http/request" 10 | "github.com/PanGan21/request-service/internal/service" 11 | "github.com/gin-contrib/cors" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func NewRouter(handler *gin.Engine, l logger.Interface, corsOrigins []string, authService auth.AuthService, requestService service.RequestService) { 16 | requestController := requestController.NewRequestController(l, requestService) 17 | // Options 18 | handler.Use(gin.Recovery()) 19 | 20 | // Cors 21 | handler.Use(cors.New(cors.Config{ 22 | AllowOrigins: corsOrigins, 23 | AllowMethods: []string{"POST", "GET", "OPTIONS"}, 24 | AllowHeaders: []string{"DNT", "X-CustomHeader", "Keep-Alive", "User-Agent", "X-Requested-With", "If-Modified-Since", "Cache-Control", "Content-Type"}, 25 | MaxAge: 12 * time.Hour, 26 | })) 27 | 28 | // K8s probe 29 | handler.GET("/healthz", func(c *gin.Context) { c.Status(http.StatusOK) }) 30 | 31 | // JWT Middleware 32 | handler.Use(auth.VerifyJWT(authService)) 33 | 34 | // Routers 35 | handler.POST("/", requestController.Create) 36 | handler.GET("/status", requestController.GetByStatus) 37 | handler.GET("/status/count", requestController.CountByStatus) 38 | handler.GET("/status/own", requestController.GetOwnByStatus) 39 | handler.GET("/status/own/count", requestController.CountOwnByStatus) 40 | 41 | requireAdminRole := []string{"ADMIN"} 42 | handler.POST("/reject", auth.AuthorizeEndpoint(requireAdminRole...), requestController.RejectRequest) 43 | handler.POST("/approve", auth.AuthorizeEndpoint(requireAdminRole...), requestController.Approve) 44 | } 45 | -------------------------------------------------------------------------------- /services/user-service/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ilyakaznacheev/cleanenv" 7 | ) 8 | 9 | type ( 10 | Config struct { 11 | App `yaml:"app"` 12 | HTTP `yaml:"http"` 13 | Log `yaml:"logger"` 14 | Postgres `yaml:"postgres"` 15 | Redis `yaml:"redis"` 16 | User `yaml:"user"` 17 | } 18 | 19 | App struct { 20 | Name string `env-required:"true" yaml:"name" env:"APP_NAME"` 21 | } 22 | 23 | // HTTP -. 24 | HTTP struct { 25 | Port string `env-required:"true" yaml:"port" env:"HTTP_PORT"` 26 | SessionSecret string `env-required:"true" yaml:"session_secret" env:"SESSION_SECRET"` 27 | AuthSecret string `env-required:"true" yaml:"auth_secret" env:"AUTH_SECRET"` 28 | CorsOrigins []string `env-required:"true" yaml:"cors_origins" env:"CORS_ORIGINS"` 29 | } 30 | 31 | // Log -. 32 | Log struct { 33 | Level string `env-required:"true" yaml:"log_level" env:"LOG_LEVEL"` 34 | } 35 | 36 | // Postgres -. 37 | Postgres struct { 38 | PoolMax int `env-required:"true" yaml:"pool_max" env:"PG_POOL_MAX"` 39 | URL string `env-required:"true" yaml:"url" env:"PG_URL"` 40 | } 41 | 42 | // Redis -. 43 | Redis struct { 44 | Url string `env-required:"true" yaml:"url" env:"REDIS_URL"` 45 | } 46 | 47 | // User -. 48 | User struct { 49 | PasswordSalt string `env-required:"true" yaml:"password_salt" env:"PASSWORD_SALT"` 50 | } 51 | ) 52 | 53 | // NewConfig returns app config. 54 | func NewConfig() (*Config, error) { 55 | cfg := &Config{} 56 | 57 | err := cleanenv.ReadConfig("./config/config.yaml", cfg) 58 | if err != nil { 59 | return nil, fmt.Errorf("config error: %w", err) 60 | } 61 | 62 | err = cleanenv.ReadEnv(cfg) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return cfg, nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/auth/auth_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/PanGan21/pkg/entity" 8 | ) 9 | 10 | var secret = "mockSecret" 11 | var service = "mockService" 12 | var route = "/route/web" 13 | var path = "/" + service + route + "?query=something" 14 | var sessionId = "mockSessionId" 15 | 16 | var userId = "mockUserId" 17 | var username = "mockUsername" 18 | var user = &entity.PublicUser{Username: username, Id: userId} 19 | var roles = []string{"mockRole1", "mockRole2"} 20 | var mockJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJtb2NrUm9sZTEiLCJtb2NrUm9sZTIiXSwicm91dGUiOiIvcm91dGUvd2ViIiwic2VydmljZSI6Im1vY2tTZXJ2aWNlIiwic2Vzc2lvbklkIjoibW9ja1Nlc3Npb25JZCIsInVzZXIiOnsiSWQiOiJtb2NrVXNlcklkIiwiVXNlcm5hbWUiOiJtb2NrVXNlcm5hbWUifX0.6kMJHS4OE9JAU3yU8DfqZYeZME7qQupD0EUy5E9ek04" 21 | 22 | func TestSingJWT(t *testing.T) { 23 | authService := NewAuthService([]byte(secret)) 24 | 25 | token, err := authService.SignJWT(sessionId, *user, path, roles...) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | if token != mockJWT { 31 | t.Fatal("incorrect jwt") 32 | } 33 | } 34 | 35 | func TestVerifyJWT(t *testing.T) { 36 | authService := NewAuthService([]byte(secret)) 37 | authData, err := authService.VerifyJWT(mockJWT, route) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | if authData.Service != service || authData.User != *user || authData.Route != route || authData.SessionId != sessionId { 43 | t.Fatal(errors.New("incorrect verification")) 44 | } 45 | } 46 | 47 | func TestMatchRoute(t *testing.T) { 48 | internalPath, err := SplitPath(path) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | if internalPath.Service != service { 54 | t.Fatal("service does not match") 55 | } 56 | 57 | if internalPath.Route != route { 58 | t.Fatal("route does not match") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/messaging/subscriber.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | kafka "github.com/segmentio/kafka-go" 11 | ) 12 | 13 | type Subscriber interface { 14 | Subscribe(topic string, fn fnHandler) 15 | } 16 | 17 | type kafkaSubscriber struct { 18 | broker string 19 | groupId string 20 | } 21 | 22 | type fnHandler func(msg Message) error 23 | 24 | func NewSubscriber(url string, groupId string) Subscriber { 25 | return &kafkaSubscriber{broker: url, groupId: groupId} 26 | } 27 | 28 | func (k kafkaSubscriber) Subscribe(topic string, fn fnHandler) { 29 | r := kafka.NewReader(kafka.ReaderConfig{ 30 | Brokers: []string{k.broker}, 31 | GroupID: k.groupId, 32 | Topic: topic, 33 | GroupBalancers: []kafka.GroupBalancer{kafka.RoundRobinGroupBalancer{}}, 34 | CommitInterval: time.Second, 35 | RebalanceTimeout: time.Second * 30, 36 | HeartbeatInterval: time.Second * 9, 37 | SessionTimeout: time.Second * 60, 38 | JoinGroupBackoff: time.Second * 25, 39 | }) 40 | 41 | defer r.Close() 42 | 43 | ctx := context.Background() 44 | 45 | for { 46 | m, err := r.FetchMessage(ctx) 47 | if err != nil { 48 | fmt.Println(err) 49 | break 50 | } 51 | 52 | var deserialized Message 53 | if err := json.Unmarshal(m.Value, &deserialized); err != nil { 54 | fmt.Println(err) 55 | break 56 | } 57 | 58 | fmt.Printf("SUBSCRIBER: Topic: %s - Partition: %d - Offset: %d - Key: %s - Payload: %v - Timestamp: %d\n", m.Topic, m.Partition, m.Offset, string(m.Key), deserialized.Payload, deserialized.Timestamp) 59 | 60 | err = fn(deserialized) 61 | if err != nil { 62 | fmt.Println(err) 63 | } 64 | 65 | if err := r.CommitMessages(ctx, m); err != nil { 66 | log.Fatal("failed to commit messages:", err) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /clients/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # service-bid-platform 2 | 3 | Scalable Auction Platform, a comprehensive system designed and developed as part of a thesis project. This project focuses on building a transparent bidding platform for service auctions, implementing microservices architecture, and adhering to clean architecture principles. 4 | 5 | ## Overview 6 | 7 | The platform leverages microservices architecture to achieve scalability, maintainability, and flexibility in handling service auctions. Clean architecture principles are followed to ensure separation of concerns, making the system modular and easy to extend. 8 | 9 | ## Getting Started 10 | 11 | Run the Backend
12 | `docker-compose up --build`
13 | Run the Frontend
14 | 15 | ``` 16 | cd clients/frontend 17 | yarn install 18 | yarn start 19 | ``` 20 | 21 | Navigate to http://localhost:3000 to access the web frontend. 22 | 23 | ## Project Structure 24 | 25 | - `pkg`: Contains reusable packages shared across microservices. 26 | - `api-gateway`: Configuration for the API gateway. 27 | clients: Houses different clients, with the current implementation being a React web frontend. 28 | - `demo`: Includes demo data for testing purposes. 29 | - `integration-tests`: Integration tests executed through Docker compose. 30 | - `scripts`: Utility scripts for the project. 31 | - `services`: Microservices, including user authentication, request handling, auction authorization, and bid management. 32 | - `ssl`: Testing SSL certificates used during development. 33 | 34 | ## Auction Mechanism 35 | 36 | The platform employs [Vickrey auctions](https://en.wikipedia.org/wiki/Vickrey_auction) to enhance bidding competition, ensuring transparency and fairness. Role-Based Access Control (RBAC) is implemented to manage user roles effectively. 37 | 38 | ## License 39 | 40 | This project is licensed under the MIT License, allowing you to use, modify, and distribute the software freely. 41 | -------------------------------------------------------------------------------- /services/auction-service/internal/service/bid.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | bidRepo "github.com/PanGan21/auction-service/internal/repository/bid" 8 | "github.com/PanGan21/pkg/entity" 9 | ) 10 | 11 | type BidService interface { 12 | Create(ctx context.Context, bid entity.Bid) error 13 | FindWinningBidByAuctionId(ctx context.Context, auctionId string) (entity.Bid, error) 14 | FindSecondWinningBidByAuctionId(ctx context.Context, auctionId string) (float64, error) 15 | } 16 | 17 | type bidService struct { 18 | bidRepo bidRepo.BidRepository 19 | } 20 | 21 | func NewBidService(br bidRepo.BidRepository) BidService { 22 | return &bidService{bidRepo: br} 23 | } 24 | 25 | func (s *bidService) Create(ctx context.Context, bid entity.Bid) error { 26 | err := s.bidRepo.Create(ctx, bid) 27 | if err != nil { 28 | return fmt.Errorf("BidService - Create - s.bidRepo.Create: %w", err) 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func (s *bidService) FindWinningBidByAuctionId(ctx context.Context, auctionId string) (entity.Bid, error) { 35 | var winnigBid entity.Bid 36 | 37 | bids, err := s.bidRepo.FindManyByAuctionIdWithMinAmount(ctx, auctionId) 38 | if err != nil { 39 | return winnigBid, fmt.Errorf("BidService - FindWinningBidByAuctionId - s.bidRepo.FindOneByAuctionIdWithMinAmount: %w", err) 40 | } 41 | 42 | if len(bids) < 1 { 43 | return winnigBid, fmt.Errorf("BidService - FindWinningBidByAuctionId - s.bidRepo.FindOneByAuctionIdWithMinAmount: Winning bid can only be one") 44 | } 45 | 46 | winnigBid = bids[0] 47 | 48 | return winnigBid, nil 49 | } 50 | 51 | func (s *bidService) FindSecondWinningBidByAuctionId(ctx context.Context, auctionId string) (float64, error) { 52 | secondWinningAmount, err := s.bidRepo.FindSecondMinAmountByAuctionId(ctx, auctionId) 53 | if err != nil { 54 | return secondWinningAmount, fmt.Errorf("BidService - FindSecondWinningBidByAuctionId - s.bidRepo.FindSecondMinAmountByAuctionId: %w", err) 55 | } 56 | 57 | return secondWinningAmount, nil 58 | } 59 | -------------------------------------------------------------------------------- /services/auction-service/internal/repository/auction/repository.go: -------------------------------------------------------------------------------- 1 | package auction 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/PanGan21/pkg/entity" 7 | "github.com/PanGan21/pkg/pagination" 8 | ) 9 | 10 | type AuctionRepository interface { 11 | Create(ctx context.Context, auction entity.Auction) (int, error) 12 | GetAll(ctx context.Context, pagination *pagination.Pagination) (*[]entity.Auction, error) 13 | CountAll(ctx context.Context) (int, error) 14 | FindOneById(ctx context.Context, id int) (entity.Auction, error) 15 | FindByCreatorId(ctx context.Context, creatorId string, pagination *pagination.Pagination) (*[]entity.Auction, error) 16 | CountByCreatorId(ctx context.Context, creatorId string) (int, error) 17 | UpdateWinningBidIdAndStatusById(ctx context.Context, id int, winningBidId string, status entity.AuctionStatus, winnerId string, winningAmount float64) (entity.Auction, error) 18 | GetAllOpenPastTime(ctx context.Context, timestamp int64, pagination *pagination.Pagination) (*[]entity.ExtendedAuction, error) 19 | CountAllOpenPastTime(ctx context.Context, timestamp int64) (int, error) 20 | UpdateStatusByAuctionId(ctx context.Context, status entity.AuctionStatus, id int) (entity.Auction, error) 21 | GetAllByStatus(ctx context.Context, status entity.AuctionStatus, pagination *pagination.Pagination) (*[]entity.Auction, error) 22 | CountAllByStatus(ctx context.Context, status entity.AuctionStatus) (int, error) 23 | GetOwnAssignedByStatuses(ctx context.Context, statuses []entity.AuctionStatus, userId string, pagination *pagination.Pagination) (*[]entity.Auction, error) 24 | CountOwnAssignedByStatuses(ctx context.Context, statuses []entity.AuctionStatus, userId string) (int, error) 25 | FindByCreatorIdAndStatus(ctx context.Context, creatorId string, status entity.AuctionStatus, pagination *pagination.Pagination) (*[]entity.Auction, error) 26 | CountByCreatorIdAndStatus(ctx context.Context, creatorId string, status entity.AuctionStatus) (int, error) 27 | UpdateDeadlineByAuctionId(ctx context.Context, deadline int64, id int) (entity.Auction, error) 28 | } 29 | -------------------------------------------------------------------------------- /services/user-service/internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/PanGan21/pkg/auth" 10 | "github.com/PanGan21/pkg/httpserver" 11 | "github.com/PanGan21/pkg/logger" 12 | "github.com/PanGan21/pkg/postgres" 13 | "github.com/PanGan21/user-service/config" 14 | userRepository "github.com/PanGan21/user-service/internal/repository/user" 15 | routes "github.com/PanGan21/user-service/internal/routes/http" 16 | "github.com/PanGan21/user-service/internal/service" 17 | "github.com/gin-gonic/contrib/sessions" 18 | "github.com/gin-gonic/gin" 19 | ) 20 | 21 | func Run(cfg *config.Config) { 22 | var err error 23 | 24 | l := logger.New(cfg.Log.Level) 25 | 26 | // Repository 27 | pg, err := postgres.New(cfg.Postgres.URL, postgres.MaxPoolSize(cfg.Postgres.PoolMax)) 28 | if err != nil { 29 | l.Fatal(fmt.Errorf("app - Run - postgres.New: %w", err)) 30 | } 31 | defer pg.Close() 32 | 33 | // Session store 34 | store, err := sessions.NewRedisStore(10, "tcp", cfg.Redis.Url, "", []byte(cfg.SessionSecret)) 35 | if err != nil { 36 | l.Fatal(err) 37 | } 38 | 39 | userRepo := userRepository.NewUserRepository(*pg) 40 | userService := service.NewUserService(userRepo, cfg.User.PasswordSalt) 41 | authService := auth.NewAuthService([]byte(cfg.AuthSecret)) 42 | 43 | // HTTP Server 44 | gin.SetMode(gin.ReleaseMode) 45 | handler := gin.Default() 46 | 47 | routes.NewRouter(handler, l, cfg.CorsOrigins, store, userService, authService) 48 | httpServer := httpserver.New(handler, httpserver.Port(cfg.HTTP.Port)) 49 | 50 | // Waiting signal 51 | interrupt := make(chan os.Signal, 1) 52 | signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) 53 | 54 | select { 55 | case s := <-interrupt: 56 | l.Info("app - Run - signal: " + s.String()) 57 | case err = <-httpServer.Notify(): 58 | l.Error(fmt.Errorf("app - Run - httpServer.Notify: %w", err)) 59 | } 60 | 61 | // Shutdown 62 | err = httpServer.Shutdown() 63 | if err != nil { 64 | l.Error(fmt.Errorf("app - Run - httpServer.Shutdown: %w", err)) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /scripts/init_postgres.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | set -eo pipefail 4 | 5 | if ! [ -x "$(command -v migrate)" ]; then 6 | echo >&2 "Error: migrate is not installed." 7 | exit 1 8 | fi 9 | 10 | if ! [ -x "$(command -v migrate)" ]; then 11 | echo >&2 "Error: migrate is not installed." 12 | echo >&2 "Use:" 13 | echo >&2 " brew install golang-migrate" 14 | echo >&2 "to install it." 15 | exit 1 16 | fi 17 | 18 | DB_USER=${POSTGRES_USER:=postgres} 19 | DB_PASSWORD="${POSTGRES_PASSWORD:=password}" 20 | DB_NAME="${POSTGRES_DB:=user}" 21 | DB_PORT="${POSTGRES_PORT:=5432}" 22 | 23 | # Allow to skip Docker if a dockerized Postgres database is already running 24 | if [[ -z "${SKIP_DOCKER}" ]] 25 | then 26 | docker build -t postgres-local -f ./scripts/Dockerfile ./scripts 27 | docker run \ 28 | -e POSTGRES_USER=${DB_USER} \ 29 | -e POSTGRES_PASSWORD=${DB_PASSWORD} \ 30 | -e POSTGRES_DB=${DB_NAME} \ 31 | -e POSTGRES_MULTIPLE_DATABASES=auction \ 32 | -p "${DB_PORT}":5432 \ 33 | -d postgres-local \ 34 | postgres -N 1000 35 | fi 36 | 37 | until PGPASSWORD="${DB_PASSWORD}" psql -h "localhost" -U "${DB_USER}" -p "${DB_PORT}" -d "postgres" -c '\q'; do 38 | >&2 echo "Postgres is still unavailable - sleeping" 39 | sleep 1 40 | done 41 | 42 | >&2 echo "Postgres is up and running on port ${DB_PORT} - running migrations now!" 43 | 44 | export DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_NAME} 45 | migrate -path ./user-service/migrations -database "postgres://${DB_USER}:${DB_PASSWORD}@localhost:${DB_PORT}/user?sslmode=disable" up 46 | migrate -path ./auction-service/migrations -database "postgres://${DB_USER}:${DB_PASSWORD}@localhost:${DB_PORT}/auction?sslmode=disable" up 47 | migrate -path ./bidding-service/migrations -database "postgres://${DB_USER}:${DB_PASSWORD}@localhost:${DB_PORT}/bidding?sslmode=disable" up 48 | migrate -path ./request-service/migrations -database "postgres://${DB_USER}:${DB_PASSWORD}@localhost:${DB_PORT}/request?sslmode=disable" up 49 | 50 | >&2 echo "Postgres has been migrated, ready to go!" -------------------------------------------------------------------------------- /clients/frontend/src/common/table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { Row, useTable } from "react-table"; 3 | import { Loader } from "../loader/Loader"; 4 | import { TableRow } from "../table-row"; 5 | import "./styles.module.css"; 6 | 7 | export type Column = { 8 | Header: string; 9 | accessor: string; 10 | }; 11 | 12 | export const AppTable = ({ 13 | columns, 14 | data, 15 | isLoading, 16 | onRowClick, 17 | }: { 18 | columns: any; 19 | data: any; 20 | isLoading: boolean; 21 | onRowClick: (row: Row) => void; 22 | }) => { 23 | const columnData = useMemo(() => columns, [columns]); 24 | const rowData = useMemo(() => data, [data]); 25 | const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = 26 | useTable({ 27 | columns: columnData, 28 | data: rowData, 29 | }); 30 | 31 | return ( 32 | <> 33 | {isLoading ? ( 34 | 35 | ) : ( 36 | <> 37 | 38 | 39 | {headerGroups.map((headerGroup) => ( 40 | 41 | {headerGroup.headers.map((column) => ( 42 | 45 | ))} 46 | 47 | ))} 48 | 49 | 50 | {rows.map((row, i) => { 51 | prepareRow(row); 52 | return ( 53 | onRowClick(row)} 56 | > 57 | {row.cells.map((cell) => { 58 | return ( 59 | 60 | ); 61 | })} 62 | 63 | ); 64 | })} 65 | 66 |
43 | {column.render("Header")} 44 |
{cell.render("Cell")}
67 | 68 | )} 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /services/request-service/internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/PanGan21/pkg/auth" 10 | "github.com/PanGan21/pkg/httpserver" 11 | "github.com/PanGan21/pkg/logger" 12 | "github.com/PanGan21/pkg/messaging" 13 | "github.com/PanGan21/pkg/postgres" 14 | "github.com/PanGan21/request-service/config" 15 | requestEvents "github.com/PanGan21/request-service/internal/events/request" 16 | requestRepository "github.com/PanGan21/request-service/internal/repository/request" 17 | routes "github.com/PanGan21/request-service/internal/routes/http" 18 | "github.com/PanGan21/request-service/internal/service" 19 | "github.com/gin-gonic/gin" 20 | ) 21 | 22 | func Run(cfg *config.Config) { 23 | var err error 24 | 25 | l := logger.New(cfg.Log.Level) 26 | 27 | // Repository 28 | pg, err := postgres.New(cfg.Postgres.URL, postgres.MaxPoolSize(cfg.Postgres.PoolMax)) 29 | if err != nil { 30 | l.Fatal(fmt.Errorf("app - Run - postgres.New: %w", err)) 31 | } 32 | defer pg.Close() 33 | 34 | // Events 35 | pub := messaging.NewPublisher(cfg.Kafka.URL, cfg.Kafka.Retries) 36 | defer pub.Close() 37 | 38 | requestRepo := requestRepository.NewRequestRepository(*pg) 39 | 40 | requestEv := requestEvents.NewRequestEvents(pub) 41 | authService := auth.NewAuthService([]byte(cfg.AuthSecret)) 42 | requestService := service.NewRequestService(requestRepo, requestEv) 43 | 44 | // HTTP Server 45 | gin.SetMode(gin.ReleaseMode) 46 | handler := gin.Default() 47 | 48 | routes.NewRouter(handler, l, cfg.CorsOrigins, authService, requestService) 49 | httpServer := httpserver.New(handler, httpserver.Port(cfg.HTTP.Port)) 50 | 51 | // Waiting signal 52 | interrupt := make(chan os.Signal, 1) 53 | signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) 54 | 55 | select { 56 | case s := <-interrupt: 57 | l.Info("app - Run - signal: " + s.String()) 58 | case err = <-httpServer.Notify(): 59 | l.Error(fmt.Errorf("app - Run - httpServer.Notify: %w", err)) 60 | } 61 | 62 | // Shutdown 63 | err = httpServer.Shutdown() 64 | if err != nil { 65 | l.Error(fmt.Errorf("app - Run - httpServer.Shutdown: %w", err)) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /services/user-service/internal/service/user.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "crypto/sha1" 6 | "errors" 7 | "fmt" 8 | "strconv" 9 | 10 | "github.com/PanGan21/pkg/entity" 11 | userRepo "github.com/PanGan21/user-service/internal/repository/user" 12 | ) 13 | 14 | var ( 15 | ErrUserNotFound = errors.New("user not found") 16 | ) 17 | 18 | type UserService interface { 19 | Login(ctx context.Context, username string, password string) (string, error) 20 | Register(ctx context.Context, username string, email string, phone string, password string) (string, error) 21 | GetById(ctx context.Context, id int) (entity.User, error) 22 | } 23 | type userService struct { 24 | userRepo userRepo.UserRepository 25 | hashSalt string 26 | } 27 | 28 | func NewUserService(ur userRepo.UserRepository, salt string) UserService { 29 | return &userService{userRepo: ur, hashSalt: salt} 30 | } 31 | 32 | func (s *userService) Login(ctx context.Context, username string, password string) (string, error) { 33 | passwordHash := s.hashPassword(password) 34 | 35 | user, err := s.userRepo.GetByUsernameAndPassword(ctx, username, passwordHash) 36 | if err != nil { 37 | return "", fmt.Errorf("UserService - Login - s.userRepo.GetByUsernameAndPassword: %w", err) 38 | } 39 | 40 | return user.Id, nil 41 | } 42 | 43 | func (s *userService) Register(ctx context.Context, username string, email string, phone string, password string) (string, error) { 44 | passwordHash := s.hashPassword(password) 45 | 46 | var defaultRoles = []string{} 47 | 48 | userId, err := s.userRepo.Create(ctx, username, email, phone, passwordHash, defaultRoles) 49 | if err != nil { 50 | return "", fmt.Errorf("UserService - Register - s.userRepo.Create: %w", err) 51 | } 52 | 53 | return strconv.Itoa(userId), nil 54 | } 55 | 56 | func (s *userService) GetById(ctx context.Context, id int) (entity.User, error) { 57 | user, err := s.userRepo.GetById(ctx, id) 58 | if err != nil { 59 | return user, err 60 | } 61 | 62 | return user, nil 63 | } 64 | 65 | func (s *userService) hashPassword(password string) string { 66 | pwd := sha1.New() 67 | pwd.Write([]byte(password)) 68 | pwd.Write([]byte(s.hashSalt)) 69 | 70 | return fmt.Sprintf("%x", pwd.Sum(nil)) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/entity/request.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "errors" 4 | 5 | type Request struct { 6 | Id int `json:"Id" db:"Id"` 7 | Title string `json:"Title" db:"Title"` 8 | Postcode string `json:"Postcode" db:"Postcode"` 9 | Info string `json:"Info" db:"Info"` 10 | CreatorId string `json:"CreatorId" db:"CreatorId"` 11 | Status RequestStatus `json:"Status" db:"Status"` 12 | RejectionReason string `json:"RejectionReason" db:"RejectionReason"` 13 | } 14 | 15 | type RequestStatus string 16 | 17 | const ( 18 | NewRequest RequestStatus = "new" 19 | ApprovedRequest RequestStatus = "approved" 20 | RejectedRequest RequestStatus = "rejected" 21 | ) 22 | 23 | var ErrIncorrectRequestType = errors.New("incorrect request type") 24 | 25 | func IsRequestType(unknown interface{}) (Request, error) { 26 | var request Request 27 | 28 | unknownMap, ok := unknown.(map[string]interface{}) 29 | if !ok { 30 | return request, ErrIncorrectRequestType 31 | } 32 | 33 | request.CreatorId, ok = unknownMap["CreatorId"].(string) 34 | if !ok { 35 | return request, ErrIncorrectRequestType 36 | } 37 | 38 | request.Info, ok = unknownMap["Info"].(string) 39 | if !ok { 40 | return request, ErrIncorrectRequestType 41 | } 42 | 43 | request.Postcode, ok = unknownMap["Postcode"].(string) 44 | if !ok { 45 | return request, ErrIncorrectRequestType 46 | } 47 | 48 | request.Title, ok = unknownMap["Title"].(string) 49 | if !ok { 50 | return request, ErrIncorrectRequestType 51 | } 52 | 53 | floatId, ok := unknownMap["Id"].(float64) 54 | if !ok { 55 | return request, ErrIncorrectRequestType 56 | } 57 | request.Id = int(floatId) 58 | 59 | s, ok := unknownMap["Status"].(string) 60 | if !ok { 61 | return request, ErrIncorrectRequestType 62 | } 63 | status := RequestStatus(s) 64 | 65 | switch status { 66 | case NewRequest, ApprovedRequest, RejectedRequest: 67 | request.Status = status 68 | default: 69 | return request, ErrIncorrectRequestType 70 | } 71 | 72 | request.RejectionReason = unknownMap["RejectionReason"].(string) 73 | if !ok { 74 | return request, ErrIncorrectRequestType 75 | } 76 | 77 | return request, nil 78 | } 79 | -------------------------------------------------------------------------------- /clients/frontend/src/components/MyBids.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Pagination } from "../common/pagination"; 3 | import { AppTable, Column } from "../common/table"; 4 | import { ROWS_PER_TABLE_PAGE } from "../constants"; 5 | import { countMyBids, getMyBids } from "../services/bid"; 6 | import { Bid } from "../types/bid"; 7 | 8 | const columns: Column[] = [ 9 | { 10 | Header: "Id", 11 | accessor: "Id", 12 | }, 13 | { 14 | Header: "Amount €", 15 | accessor: "Amount", 16 | }, 17 | { 18 | Header: "AuctionId", 19 | accessor: "AuctionId", 20 | }, 21 | ]; 22 | 23 | type Props = {}; 24 | 25 | export const MyBids: React.FC = () => { 26 | const [pageData, setPageData] = useState<{ 27 | rowData: Bid[]; 28 | isLoading: boolean; 29 | totalBids: number; 30 | }>({ 31 | rowData: [], 32 | isLoading: false, 33 | totalBids: 0, 34 | }); 35 | const [totalBids, setTotalBids] = useState(0); 36 | const [currentPage, setCurrentPage] = useState(1); 37 | 38 | useEffect(() => { 39 | setPageData((prevState) => ({ 40 | ...prevState, 41 | rowData: [], 42 | isLoading: true, 43 | })); 44 | 45 | countMyBids().then((response) => { 46 | if (response.data && response.data) { 47 | setTotalBids(response.data); 48 | } 49 | }); 50 | 51 | getMyBids(ROWS_PER_TABLE_PAGE, currentPage).then((response) => { 52 | const bids = response.data || []; 53 | setPageData({ 54 | isLoading: false, 55 | rowData: bids, 56 | totalBids, 57 | }); 58 | }); 59 | }, [currentPage, totalBids]); 60 | 61 | const handleRowSelection = (auction: any) => {}; 62 | 63 | return ( 64 |
65 |
66 | handleRowSelection(r.values)} 71 | /> 72 |
73 | 79 |
80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /clients/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /clients/frontend/src/services/request.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { API_URL } from "../constants"; 3 | import { NewRequest } from "../types/request"; 4 | 5 | export const createRequest = async (newRequest: NewRequest) => { 6 | return axios.post(API_URL + "/request/", newRequest, { 7 | withCredentials: true, 8 | }); 9 | }; 10 | 11 | export const rejectRequest = async ( 12 | requestId: string, 13 | rejectionReason: string 14 | ) => { 15 | return axios.post( 16 | API_URL + `/request/reject?requestId=${requestId}`, 17 | { 18 | rejectionReason, 19 | }, 20 | { withCredentials: true } 21 | ); 22 | }; 23 | 24 | export const approveRequest = async (requestId: string, days: number) => { 25 | return axios.post( 26 | API_URL + `/request/approve?requestId=${requestId}&days=${days}`, 27 | {}, 28 | { withCredentials: true } 29 | ); 30 | }; 31 | 32 | export const getMyRequests = async (limit: number, page: number) => { 33 | return axios.get( 34 | API_URL + 35 | `/request/status/own?status=new&limit=${limit}&page=${page}&asc=false`, 36 | { 37 | withCredentials: true, 38 | } 39 | ); 40 | }; 41 | 42 | export const countMyRequests = async () => { 43 | return axios.get(API_URL + "/request/status/own/count?status=new", { 44 | withCredentials: true, 45 | }); 46 | }; 47 | 48 | export const getOwnRejectedRequests = async (limit: number, page: number) => { 49 | return axios.get( 50 | API_URL + 51 | `/request/status/own?status=rejected&limit=${limit}&page=${page}&asc=false`, 52 | { withCredentials: true } 53 | ); 54 | }; 55 | 56 | export const countRequestsByStatus = async (status: string) => { 57 | return axios.get(API_URL + `/request/status/count?status=${status}`, { 58 | withCredentials: true, 59 | }); 60 | }; 61 | 62 | export const getRequestsByStatus = async ( 63 | status: string, 64 | limit: number, 65 | page: number 66 | ) => { 67 | return axios.get( 68 | API_URL + 69 | `/request/status?status=${status}&limit=${limit}&page=${page}&asc=false`, 70 | { withCredentials: true } 71 | ); 72 | }; 73 | 74 | export const countOwnRejectedRequests = async () => { 75 | return axios.get(API_URL + "/request/status/own/count?status=rejected", { 76 | withCredentials: true, 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /pkg/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/PanGan21/pkg/entity" 8 | "github.com/golang-jwt/jwt/v4" 9 | "github.com/mitchellh/mapstructure" 10 | ) 11 | 12 | type AuthService interface { 13 | SignJWT(sessionId string, user entity.PublicUser, path string, roles ...string) (string, error) 14 | VerifyJWT(encoded string, route string) (AuthTokenData, error) 15 | } 16 | 17 | type authService struct { 18 | secret []byte 19 | } 20 | 21 | func NewAuthService(secret []byte) AuthService { 22 | return &authService{secret: secret} 23 | } 24 | 25 | func (s *authService) SignJWT(sessionId string, user entity.PublicUser, path string, roles ...string) (string, error) { 26 | internalPath, _ := SplitPath(path) 27 | 28 | token := jwt.New(jwt.SigningMethodHS256) 29 | claims := token.Claims.(jwt.MapClaims) 30 | claims["service"] = internalPath.Service 31 | claims["route"] = internalPath.Route 32 | claims["sessionId"] = sessionId 33 | claims["user"] = user 34 | claims["roles"] = roles 35 | 36 | tokenString, err := token.SignedString(s.secret) 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | return tokenString, nil 42 | } 43 | 44 | func (s *authService) VerifyJWT(encoded string, route string) (AuthTokenData, error) { 45 | var user entity.PublicUser 46 | 47 | authTokenData := AuthTokenData{ 48 | Service: "", 49 | Route: "", 50 | SessionId: "", 51 | User: user, 52 | Roles: make([]string, 0), 53 | } 54 | 55 | token, err := jwt.Parse(encoded, func(token *jwt.Token) (interface{}, error) { 56 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 57 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 58 | } 59 | return s.secret, nil 60 | }) 61 | if err != nil { 62 | return authTokenData, err 63 | } 64 | 65 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 66 | authTokenData.Service = claims["service"].(string) 67 | authTokenData.Route = claims["route"].(string) 68 | authTokenData.SessionId = claims["sessionId"].(string) 69 | mapstructure.Decode(claims["user"], &authTokenData.User) 70 | parsedRoles := claims["roles"].([]interface{}) 71 | 72 | for _, role := range parsedRoles { 73 | authTokenData.Roles = append(authTokenData.Roles, fmt.Sprintf("%s", role)) 74 | } 75 | } 76 | 77 | if authTokenData.Route != route { 78 | return authTokenData, errors.New("incorrect route") 79 | } 80 | 81 | return authTokenData, nil 82 | } 83 | -------------------------------------------------------------------------------- /services/user-service/internal/repository/user/user_postgres.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/PanGan21/pkg/entity" 8 | "github.com/PanGan21/pkg/postgres" 9 | ) 10 | 11 | type userRepository struct { 12 | db postgres.Postgres 13 | } 14 | 15 | func NewUserRepository(db postgres.Postgres) *userRepository { 16 | return &userRepository{db: db} 17 | } 18 | 19 | func (repo *userRepository) GetByUsernameAndPassword(ctx context.Context, username string, password string) (entity.User, error) { 20 | var user entity.User 21 | 22 | c, err := repo.db.Pool.Acquire(ctx) 23 | if err != nil { 24 | return user, err 25 | } 26 | defer c.Release() 27 | 28 | const query = ` 29 | SELECT Id::varchar(255), Username, Email, Phone, PasswordHash, Roles FROM users 30 | WHERE Username=$1 AND PasswordHash=$2; 31 | ` 32 | 33 | row := c.QueryRow(ctx, query, username, password) 34 | 35 | err = row.Scan(&user.Id, &user.Username, &user.Email, &user.Phone, &user.PasswordHash, &user.Roles) 36 | if err != nil { 37 | return user, fmt.Errorf("UserRepo - GetByUsernameAndPassword - row.Scan: %w", err) 38 | } 39 | 40 | return user, nil 41 | } 42 | 43 | func (repo *userRepository) Create(ctx context.Context, username string, email string, phone string, passwordHash string, roles []string) (int, error) { 44 | var userId int 45 | 46 | c, err := repo.db.Pool.Acquire(ctx) 47 | if err != nil { 48 | return userId, err 49 | } 50 | defer c.Release() 51 | 52 | const query = ` 53 | INSERT INTO users (Username, Email, Phone, PasswordHash, Roles) 54 | VALUES ($1, $2, $3, $4, $5) RETURNING Id; 55 | ` 56 | 57 | err = c.QueryRow(ctx, query, username, email, phone, passwordHash, roles).Scan(&userId) 58 | if err != nil { 59 | return userId, fmt.Errorf("UserRepo - Create - c.Exec: %w", err) 60 | } 61 | return userId, nil 62 | } 63 | 64 | func (repo *userRepository) GetById(ctx context.Context, id int) (entity.User, error) { 65 | var user entity.User 66 | 67 | c, err := repo.db.Pool.Acquire(ctx) 68 | if err != nil { 69 | return user, err 70 | } 71 | defer c.Release() 72 | 73 | const query = ` 74 | SELECT Id::varchar(255), Username, Email, Phone, PasswordHash, Roles FROM users 75 | WHERE Id=$1; 76 | ` 77 | 78 | row := c.QueryRow(ctx, query, id) 79 | err = row.Scan(&user.Id, &user.Username, &user.Email, &user.Phone, &user.PasswordHash, &user.Roles) 80 | if err != nil { 81 | return user, fmt.Errorf("UserRepo - GetById - row.Scan: %w", err) 82 | } 83 | 84 | return user, nil 85 | } 86 | -------------------------------------------------------------------------------- /services/bidding-service/internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/PanGan21/bidding-service/config" 10 | bidEvents "github.com/PanGan21/bidding-service/internal/events/bid" 11 | auctionRepository "github.com/PanGan21/bidding-service/internal/repository/auction" 12 | bidRepository "github.com/PanGan21/bidding-service/internal/repository/bid" 13 | "github.com/PanGan21/bidding-service/internal/routes/events" 14 | routes "github.com/PanGan21/bidding-service/internal/routes/http" 15 | "github.com/PanGan21/bidding-service/internal/service" 16 | "github.com/PanGan21/pkg/auth" 17 | "github.com/PanGan21/pkg/httpserver" 18 | "github.com/PanGan21/pkg/logger" 19 | "github.com/PanGan21/pkg/messaging" 20 | "github.com/PanGan21/pkg/postgres" 21 | "github.com/gin-gonic/gin" 22 | ) 23 | 24 | func Run(cfg *config.Config) { 25 | var err error 26 | 27 | l := logger.New(cfg.Log.Level) 28 | 29 | // Repository 30 | pg, err := postgres.New(cfg.Postgres.URL, postgres.MaxPoolSize(cfg.Postgres.PoolMax)) 31 | if err != nil { 32 | l.Fatal(fmt.Errorf("app - Run - postgres.New: %w", err)) 33 | } 34 | defer pg.Close() 35 | 36 | // Events 37 | sub := messaging.NewSubscriber(cfg.Kafka.URL, cfg.App.Name) 38 | pub := messaging.NewPublisher(cfg.Kafka.URL, cfg.Kafka.Retries) 39 | defer pub.Close() 40 | 41 | auctionRepo := auctionRepository.NewAuctionRepository(*pg) 42 | bidRepo := bidRepository.NewBidRepository(*pg) 43 | 44 | authService := auth.NewAuthService([]byte(cfg.AuthSecret)) 45 | bidEv := bidEvents.NewBidEvents(pub) 46 | auctionService := service.NewAuctionService(auctionRepo) 47 | bidService := service.NewBidService(bidRepo, bidEv) 48 | 49 | // HTTP Server 50 | gin.SetMode(gin.ReleaseMode) 51 | handler := gin.Default() 52 | 53 | routes.NewRouter(handler, l, cfg.CorsOrigins, authService, bidService, auctionService) 54 | httpServer := httpserver.New(handler, httpserver.Port(cfg.HTTP.Port)) 55 | 56 | // Waiting signal 57 | interrupt := make(chan os.Signal, 1) 58 | signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) 59 | 60 | events.NewEventsClient(sub, l, auctionService) 61 | 62 | select { 63 | case s := <-interrupt: 64 | l.Info("app - Run - signal: " + s.String()) 65 | case err = <-httpServer.Notify(): 66 | l.Error(fmt.Errorf("app - Run - httpServer.Notify: %w", err)) 67 | } 68 | 69 | // Shutdown 70 | err = httpServer.Shutdown() 71 | if err != nil { 72 | l.Error(fmt.Errorf("app - Run - httpServer.Shutdown: %w", err)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /clients/frontend/src/components/MyServiceRequests.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { AppTable, Column } from "../common/table"; 3 | import { Pagination } from "../common/pagination"; 4 | import { ROWS_PER_TABLE_PAGE } from "../constants"; 5 | import { countMyRequests, getMyRequests } from "../services/request"; 6 | import { Request } from "../types/request"; 7 | 8 | const columns: Column[] = [ 9 | { 10 | Header: "Id", 11 | accessor: "Id", 12 | }, 13 | { 14 | Header: "Title", 15 | accessor: "Title", 16 | }, 17 | { 18 | Header: "Postcode", 19 | accessor: "Postcode", 20 | }, 21 | { 22 | Header: "Info", 23 | accessor: "Info", 24 | }, 25 | { 26 | Header: "Status", 27 | accessor: "Status", 28 | }, 29 | ]; 30 | 31 | type Props = {}; 32 | 33 | export const MyServiceRequests: React.FC = () => { 34 | const [pageData, setPageData] = useState<{ 35 | rowData: Request[]; 36 | isLoading: boolean; 37 | totalServiceRequests: number; 38 | }>({ 39 | rowData: [], 40 | isLoading: false, 41 | totalServiceRequests: 0, 42 | }); 43 | const [totalServiceRequests, setTotalServiceRequests] = useState(0); 44 | const [currentPage, setCurrentPage] = useState(1); 45 | 46 | useEffect(() => { 47 | setPageData((prevState) => ({ 48 | ...prevState, 49 | rowData: [], 50 | isLoading: true, 51 | })); 52 | 53 | countMyRequests().then((response) => { 54 | if (response.data && response.data) { 55 | setTotalServiceRequests(response.data); 56 | } 57 | }); 58 | 59 | getMyRequests(ROWS_PER_TABLE_PAGE, currentPage).then((response) => { 60 | const requests = response.data || []; 61 | setPageData({ 62 | isLoading: false, 63 | rowData: requests, 64 | totalServiceRequests: totalServiceRequests, 65 | }); 66 | }); 67 | }, [currentPage, totalServiceRequests]); 68 | 69 | const handleRowSelection = () => {}; 70 | 71 | return ( 72 |
73 |
74 | handleRowSelection()} 79 | /> 80 |
81 | 87 |
88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /services/auction-service/internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/PanGan21/auction-service/config" 10 | auctionEvents "github.com/PanGan21/auction-service/internal/events/auction" 11 | auctionRepository "github.com/PanGan21/auction-service/internal/repository/auction" 12 | bidRepository "github.com/PanGan21/auction-service/internal/repository/bid" 13 | "github.com/PanGan21/auction-service/internal/routes/events" 14 | routes "github.com/PanGan21/auction-service/internal/routes/http" 15 | "github.com/PanGan21/auction-service/internal/service" 16 | "github.com/PanGan21/pkg/auth" 17 | "github.com/PanGan21/pkg/httpserver" 18 | "github.com/PanGan21/pkg/logger" 19 | "github.com/PanGan21/pkg/messaging" 20 | "github.com/PanGan21/pkg/postgres" 21 | "github.com/gin-gonic/gin" 22 | ) 23 | 24 | func Run(cfg *config.Config) { 25 | var err error 26 | 27 | l := logger.New(cfg.Log.Level) 28 | 29 | // Repository 30 | pg, err := postgres.New(cfg.Postgres.URL, postgres.MaxPoolSize(cfg.Postgres.PoolMax)) 31 | if err != nil { 32 | l.Fatal(fmt.Errorf("app - Run - postgres.New: %w", err)) 33 | } 34 | defer pg.Close() 35 | 36 | // Events 37 | sub := messaging.NewSubscriber(cfg.Kafka.URL, cfg.App.Name) 38 | pub := messaging.NewPublisher(cfg.Kafka.URL, cfg.Kafka.Retries) 39 | defer pub.Close() 40 | 41 | auctionRepo := auctionRepository.NewAuctionRepository(*pg) 42 | bidRepo := bidRepository.NewBidRepository(*pg) 43 | 44 | auctionEv := auctionEvents.NewAuctionEvents(pub) 45 | authService := auth.NewAuthService([]byte(cfg.AuthSecret)) 46 | bidService := service.NewBidService(bidRepo) 47 | auctionService := service.NewAuctionService(auctionRepo, auctionEv) 48 | 49 | // HTTP Server 50 | gin.SetMode(gin.ReleaseMode) 51 | handler := gin.Default() 52 | 53 | routes.NewRouter(handler, l, cfg.CorsOrigins, authService, auctionService, bidService) 54 | httpServer := httpserver.New(handler, httpserver.Port(cfg.HTTP.Port)) 55 | 56 | // Waiting signal 57 | interrupt := make(chan os.Signal, 1) 58 | signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) 59 | 60 | events.NewEventsClient(sub, l, bidService, auctionService) 61 | 62 | select { 63 | case s := <-interrupt: 64 | l.Info("app - Run - signal: " + s.String()) 65 | case err = <-httpServer.Notify(): 66 | l.Error(fmt.Errorf("app - Run - httpServer.Notify: %w", err)) 67 | } 68 | 69 | // Shutdown 70 | err = httpServer.Shutdown() 71 | if err != nil { 72 | l.Error(fmt.Errorf("app - Run - httpServer.Shutdown: %w", err)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /services/auction-service/internal/routes/http/routes.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | auctionController "github.com/PanGan21/auction-service/internal/routes/http/auction" 8 | "github.com/PanGan21/auction-service/internal/service" 9 | "github.com/PanGan21/pkg/auth" 10 | "github.com/PanGan21/pkg/logger" 11 | "github.com/gin-contrib/cors" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func NewRouter(handler *gin.Engine, l logger.Interface, corsOrigins []string, authService auth.AuthService, auctionService service.AuctionService, bidService service.BidService) { 16 | auctionController := auctionController.NewAuctionController(l, auctionService, bidService) 17 | // Options 18 | handler.Use(gin.Recovery()) 19 | 20 | // Cors 21 | handler.Use(cors.New(cors.Config{ 22 | AllowOrigins: corsOrigins, 23 | AllowMethods: []string{"POST", "GET", "OPTIONS"}, 24 | AllowHeaders: []string{"DNT", "X-CustomHeader", "Keep-Alive", "User-Agent", "X-Requested-With", "If-Modified-Since", "Cache-Control", "Content-Type"}, 25 | MaxAge: 12 * time.Hour, 26 | })) 27 | 28 | // K8s probe 29 | handler.GET("/healthz", func(c *gin.Context) { c.Status(http.StatusOK) }) 30 | 31 | // JWT Middleware 32 | handler.Use(auth.VerifyJWT(authService)) 33 | 34 | // Routers 35 | handler.GET("/", auctionController.GetAll) 36 | handler.GET("/count", auctionController.CountAll) 37 | handler.GET("/count/own", auctionController.CountOwn) 38 | handler.GET("/own", auctionController.GetOwn) 39 | handler.GET("/own/assigned-bids", auctionController.GetOwnAssignedByStatuses) 40 | handler.GET("/own/assigned-bids/count", auctionController.CountOwnAssignedByStatuses) 41 | handler.GET("/status", auctionController.GetByStatus) 42 | handler.GET("/status/count", auctionController.CountByStatus) 43 | 44 | requireAdminRole := []string{"ADMIN"} 45 | handler.GET("/open/past-deadline", auth.AuthorizeEndpoint(requireAdminRole...), auctionController.GetOpenPastDeadline) 46 | handler.GET("/open/past-deadline/count", auth.AuthorizeEndpoint(requireAdminRole...), auctionController.CountOpenPastDeadline) 47 | handler.POST("/update/winner", auth.AuthorizeEndpoint(requireAdminRole...), auctionController.UpdateWinnerByAuctionId) 48 | handler.POST("/update/status", auth.AuthorizeEndpoint(requireAdminRole...), auctionController.UpdateStatus) 49 | handler.POST("/update/deadline", auth.AuthorizeEndpoint(requireAdminRole...), auctionController.ExtendDeadline) 50 | 51 | var requiredRoles []string 52 | handler.GET("/hello", auth.AuthorizeEndpoint(requiredRoles...), func(c *gin.Context) { c.Status(http.StatusOK) }) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/rs/zerolog" 9 | ) 10 | 11 | // Interface -. 12 | type Interface interface { 13 | Debug(message interface{}, args ...interface{}) 14 | Info(message string, args ...interface{}) 15 | Warn(message string, args ...interface{}) 16 | Error(message interface{}, args ...interface{}) 17 | Fatal(message interface{}, args ...interface{}) 18 | } 19 | 20 | // Logger -. 21 | type Logger struct { 22 | logger *zerolog.Logger 23 | } 24 | 25 | var _ Interface = (*Logger)(nil) 26 | 27 | // New -. 28 | func New(level string) *Logger { 29 | var l zerolog.Level 30 | 31 | switch strings.ToLower(level) { 32 | case "error": 33 | l = zerolog.ErrorLevel 34 | case "warn": 35 | l = zerolog.WarnLevel 36 | case "info": 37 | l = zerolog.InfoLevel 38 | case "debug": 39 | l = zerolog.DebugLevel 40 | default: 41 | l = zerolog.InfoLevel 42 | } 43 | 44 | zerolog.SetGlobalLevel(l) 45 | 46 | skipFrameCount := 3 47 | logger := zerolog.New(os.Stdout).With().Timestamp().CallerWithSkipFrameCount(zerolog.CallerSkipFrameCount + skipFrameCount).Logger() 48 | 49 | return &Logger{ 50 | logger: &logger, 51 | } 52 | } 53 | 54 | // Debug -. 55 | func (l *Logger) Debug(message interface{}, args ...interface{}) { 56 | l.msg("debug", message, args...) 57 | } 58 | 59 | // Info -. 60 | func (l *Logger) Info(message string, args ...interface{}) { 61 | l.log(message, args...) 62 | } 63 | 64 | // Warn -. 65 | func (l *Logger) Warn(message string, args ...interface{}) { 66 | l.log(message, args...) 67 | } 68 | 69 | // Error -. 70 | func (l *Logger) Error(message interface{}, args ...interface{}) { 71 | if l.logger.GetLevel() == zerolog.DebugLevel { 72 | l.Debug(message, args...) 73 | } 74 | 75 | l.msg("error", message, args...) 76 | } 77 | 78 | // Fatal -. 79 | func (l *Logger) Fatal(message interface{}, args ...interface{}) { 80 | l.msg("fatal", message, args...) 81 | 82 | os.Exit(1) 83 | } 84 | 85 | func (l *Logger) log(message string, args ...interface{}) { 86 | if len(args) == 0 { 87 | l.logger.Info().Msg(message) 88 | } else { 89 | l.logger.Info().Msgf(message, args...) 90 | } 91 | } 92 | 93 | func (l *Logger) msg(level string, message interface{}, args ...interface{}) { 94 | switch msg := message.(type) { 95 | case error: 96 | l.log(msg.Error(), args...) 97 | case string: 98 | l.log(msg, args...) 99 | default: 100 | l.log(fmt.Sprintf("%s message %v has unknown type %v", level, message, msg), args...) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /demo/request.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/http/cookiejar" 10 | "strconv" 11 | 12 | "github.com/PanGan21/demo/data" 13 | "github.com/PanGan21/pkg/entity" 14 | ) 15 | 16 | var requestApi = getBasePath("request") 17 | 18 | func createRequest(session string, requestData data.RequestData) (entity.Request, error) { 19 | createRequestPath := requestApi + "/" 20 | 21 | var request entity.Request 22 | 23 | body, err := json.Marshal(requestData) 24 | if err != nil { 25 | return request, err 26 | } 27 | 28 | jar, err := cookiejar.New(nil) 29 | if err != nil { 30 | return request, err 31 | } 32 | 33 | client := http.Client{ 34 | Jar: jar, 35 | } 36 | 37 | cookie := &http.Cookie{ 38 | Name: "s.id", 39 | Value: session, 40 | } 41 | 42 | req, err := http.NewRequest("POST", createRequestPath, bytes.NewBuffer(body)) 43 | if err != nil { 44 | return request, err 45 | } 46 | 47 | req.AddCookie(cookie) 48 | res, err := client.Do(req) 49 | if err != nil { 50 | return request, err 51 | } 52 | 53 | if res.StatusCode != 200 { 54 | return request, fmt.Errorf("request creation failed") 55 | } 56 | 57 | resBody, err := io.ReadAll(res.Body) 58 | if err != nil { 59 | return request, err 60 | } 61 | 62 | err = json.Unmarshal(resBody, &request) 63 | if err != nil { 64 | return request, err 65 | } 66 | 67 | fmt.Println("Request created!", request) 68 | 69 | return request, nil 70 | } 71 | 72 | func approveRequest(session string, requestId string, days int) error { 73 | updateRequestPath := requestApi + "/approve?requestId=" + requestId + "&days=" + strconv.Itoa(days) 74 | 75 | jar, err := cookiejar.New(nil) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | client := http.Client{ 81 | Jar: jar, 82 | } 83 | 84 | cookie := &http.Cookie{ 85 | Name: "s.id", 86 | Value: session, 87 | } 88 | 89 | req, err := http.NewRequest("POST", updateRequestPath, nil) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | req.AddCookie(cookie) 95 | res, err := client.Do(req) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | if res.StatusCode != 200 { 101 | return fmt.Errorf("request update failed") 102 | } 103 | 104 | resBody, err := io.ReadAll(res.Body) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | var request entity.Request 110 | 111 | err = json.Unmarshal(resBody, &request) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | fmt.Println("Request updated!", request) 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /services/bidding-service/internal/repository/auction/auction_postgres.go: -------------------------------------------------------------------------------- 1 | package auction 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/PanGan21/pkg/entity" 8 | "github.com/PanGan21/pkg/postgres" 9 | ) 10 | 11 | type auctionRepository struct { 12 | db postgres.Postgres 13 | } 14 | 15 | func NewAuctionRepository(db postgres.Postgres) *auctionRepository { 16 | return &auctionRepository{db: db} 17 | } 18 | 19 | func (repo *auctionRepository) Create(ctx context.Context, auction entity.Auction) error { 20 | var auctionId int 21 | 22 | c, err := repo.db.Pool.Acquire(ctx) 23 | if err != nil { 24 | return err 25 | } 26 | defer c.Release() 27 | 28 | const query = ` 29 | INSERT INTO auctions (Id, CreatorId, Info, Postcode, Title, Deadline, Status, WinningBidId, WinnerId, WinningAmount) 30 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING Id; 31 | ` 32 | 33 | c.QueryRow(ctx, query, auction.Id, auction.CreatorId, auction.Info, auction.Postcode, auction.Title, auction.Deadline, auction.Status, auction.WinningBidId, auction.WinnerId, auction.WinningAmount).Scan(&auctionId) 34 | if err != nil { 35 | return fmt.Errorf("AuctionRepo - Create - c.Exec: %w", err) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func (repo *auctionRepository) UpdateOne(ctx context.Context, auction entity.Auction) error { 42 | var auctionId int 43 | 44 | c, err := repo.db.Pool.Acquire(ctx) 45 | if err != nil { 46 | return err 47 | } 48 | defer c.Release() 49 | 50 | const query = ` 51 | UPDATE auctions SET CreatorId=$1, Info=$2, Postcode=$3, Title=$4, Deadline=$5, Status=$6, WinningBidId=$7, WinnerId=$8, WinningAmount=$9 WHERE Id=$10 RETURNING Id; 52 | ` 53 | 54 | c.QueryRow(ctx, query, auction.CreatorId, auction.Info, auction.Postcode, auction.Title, auction.Deadline, auction.Status, auction.WinningBidId, auction.WinnerId, auction.WinningAmount, auction.Id).Scan(&auctionId) 55 | if err != nil { 56 | return fmt.Errorf("AuctionRepo - Create - c.Exec: %w", err) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (repo *auctionRepository) FindOneById(ctx context.Context, id int) (entity.Auction, error) { 63 | var auction entity.Auction 64 | 65 | c, err := repo.db.Pool.Acquire(ctx) 66 | if err != nil { 67 | return auction, err 68 | } 69 | defer c.Release() 70 | 71 | const query = ` 72 | SELECT * FROM auctions WHERE Id=$1; 73 | ` 74 | 75 | err = c.QueryRow(ctx, query, id).Scan(&auction.Id, &auction.Title, &auction.Postcode, &auction.Info, &auction.CreatorId, &auction.Deadline, &auction.Status, &auction.WinningBidId, &auction.WinnerId, &auction.WinningAmount) 76 | if err != nil { 77 | return auction, fmt.Errorf("AuctionRepo - FindOneById - c.Exec: %w", err) 78 | } 79 | 80 | return auction, nil 81 | } 82 | -------------------------------------------------------------------------------- /api-gateway/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | upstream user-backend { 7 | server user-service:8000 weight=1; 8 | } 9 | 10 | upstream request-backend { 11 | server request-service:8000 weight=1; 12 | } 13 | 14 | upstream auction-backend { 15 | server auction-service:8000 weight=1; 16 | } 17 | 18 | upstream bidding-backend { 19 | server bidding-service:8000 weight=1; 20 | } 21 | 22 | server { 23 | listen 443 ssl; 24 | listen [::]:443 ssl; 25 | listen 80; 26 | server_name localhost; 27 | ssl_certificate /etc/ssl/cert.pem; 28 | ssl_certificate_key /etc/ssl/key.pem; 29 | 30 | location /user/ { 31 | include /etc/nginx/api_proxy.conf; 32 | proxy_pass http://user-backend/; 33 | } 34 | 35 | location /auth/ { 36 | include /etc/nginx/api_proxy.conf; 37 | 38 | internal; 39 | proxy_pass http://user-backend/; 40 | proxy_pass_request_body off; 41 | proxy_set_header Content-Length ""; 42 | proxy_set_header X-Real-Ip $remote_addr; 43 | proxy_set_header Authorization $http_authorization; 44 | proxy_set_header X-Forwarded-Proto $scheme; 45 | 46 | proxy_set_header X-Forwarded-Method $request_method; 47 | proxy_set_header X-Forwarded-Uri $request_uri; 48 | } 49 | 50 | location /request/ { 51 | include /etc/nginx/api_proxy.conf; 52 | 53 | auth_request /auth/authenticate; 54 | auth_request_set $auth_status $upstream_status; 55 | 56 | auth_request_set $token $upstream_http_x_internal_jwt; 57 | proxy_set_header X-Internal-Jwt $token; 58 | 59 | 60 | proxy_pass http://request-backend/; 61 | } 62 | 63 | location /auction/ { 64 | include /etc/nginx/api_proxy.conf; 65 | 66 | auth_request /auth/authenticate; 67 | auth_request_set $auth_status $upstream_status; 68 | 69 | auth_request_set $token $upstream_http_x_internal_jwt; 70 | proxy_set_header X-Internal-Jwt $token; 71 | 72 | 73 | proxy_pass http://auction-backend/; 74 | } 75 | 76 | location /bidding/ { 77 | include /etc/nginx/api_proxy.conf; 78 | 79 | auth_request /auth/authenticate; 80 | auth_request_set $auth_status $upstream_status; 81 | 82 | auth_request_set $token $upstream_http_x_internal_jwt; 83 | proxy_set_header X-Internal-Jwt $token; 84 | 85 | proxy_pass http://bidding-backend/; 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /clients/frontend/src/components/MyRejectedRequests.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Column } from "react-table"; 3 | import { Pagination } from "../common/pagination"; 4 | import { AppTable } from "../common/table"; 5 | import { ROWS_PER_TABLE_PAGE } from "../constants"; 6 | import { 7 | countOwnRejectedRequests, 8 | getOwnRejectedRequests, 9 | } from "../services/request"; 10 | import { Request } from "../types/request"; 11 | 12 | const columns: Column[] = [ 13 | { 14 | Header: "Id", 15 | accessor: "Id", 16 | }, 17 | { 18 | Header: "Title", 19 | accessor: "Title", 20 | }, 21 | { 22 | Header: "Postcode", 23 | accessor: "Postcode", 24 | }, 25 | { 26 | Header: "Info", 27 | accessor: "Info", 28 | }, 29 | { 30 | Header: "Status", 31 | accessor: "Status", 32 | }, 33 | { 34 | Header: "Rejection reason", 35 | accessor: "RejectionReason", 36 | }, 37 | ]; 38 | 39 | type Props = {}; 40 | 41 | export const MyRejectedRequests: React.FC = () => { 42 | const [pageData, setPageData] = useState<{ 43 | rowData: Request[]; 44 | isLoading: boolean; 45 | totalRequest: number; 46 | }>({ 47 | rowData: [], 48 | isLoading: false, 49 | totalRequest: 0, 50 | }); 51 | 52 | const [totalRequest, setTotalRequest] = useState(0); 53 | const [currentPage, setCurrentPage] = useState(1); 54 | 55 | useEffect(() => { 56 | setPageData((prevState) => ({ 57 | ...prevState, 58 | rowData: [], 59 | isLoading: true, 60 | })); 61 | 62 | countOwnRejectedRequests().then((response) => { 63 | if (response.data && response.data) { 64 | setTotalRequest(response.data); 65 | } 66 | }); 67 | 68 | getOwnRejectedRequests(ROWS_PER_TABLE_PAGE, currentPage).then( 69 | (response) => { 70 | const requests = response.data || []; 71 | setPageData({ 72 | isLoading: false, 73 | rowData: requests, 74 | totalRequest: totalRequest, 75 | }); 76 | } 77 | ); 78 | }, [currentPage, totalRequest]); 79 | 80 | const handleRowSelection = (request: any) => {}; 81 | 82 | return ( 83 |
84 |
85 | handleRowSelection(r.values)} 90 | /> 91 |
92 | 98 |
99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /clients/frontend/src/components/ClosedAuctions.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Column } from "react-table"; 3 | import { Pagination } from "../common/pagination"; 4 | import { AppTable } from "../common/table"; 5 | import { ROWS_PER_TABLE_PAGE } from "../constants"; 6 | import { 7 | countAuctionsByStatus, 8 | formatAuctions, 9 | getAuctionsByStatus, 10 | } from "../services/auction"; 11 | import { FormattedAuction } from "../types/auction"; 12 | 13 | const columns: Column[] = [ 14 | { 15 | Header: "Id", 16 | accessor: "Id", 17 | }, 18 | { 19 | Header: "Title", 20 | accessor: "Title", 21 | }, 22 | { 23 | Header: "Postcode", 24 | accessor: "Postcode", 25 | }, 26 | { 27 | Header: "Info", 28 | accessor: "Info", 29 | }, 30 | { 31 | Header: "Deadline", 32 | accessor: "Deadline", 33 | }, 34 | { 35 | Header: "Status", 36 | accessor: "Status", 37 | }, 38 | ]; 39 | 40 | const STATUS = "closed"; 41 | 42 | type Props = {}; 43 | 44 | export const ClosedAuctions: React.FC = () => { 45 | const [pageData, setPageData] = useState<{ 46 | rowData: FormattedAuction[]; 47 | isLoading: boolean; 48 | totalAuctions: number; 49 | }>({ 50 | rowData: [], 51 | isLoading: false, 52 | totalAuctions: 0, 53 | }); 54 | 55 | const [totalAuctions, setTotalAuctions] = useState(0); 56 | const [currentPage, setCurrentPage] = useState(1); 57 | 58 | useEffect(() => { 59 | setPageData((prevState) => ({ 60 | ...prevState, 61 | rowData: [], 62 | isLoading: true, 63 | })); 64 | 65 | countAuctionsByStatus(STATUS).then((response) => { 66 | if (response.data && response.data) { 67 | setTotalAuctions(response.data); 68 | } 69 | }); 70 | 71 | getAuctionsByStatus(STATUS, ROWS_PER_TABLE_PAGE, currentPage).then( 72 | (response) => { 73 | const auctions = response.data || []; 74 | setPageData({ 75 | isLoading: false, 76 | rowData: formatAuctions(auctions), 77 | totalAuctions: totalAuctions, 78 | }); 79 | } 80 | ); 81 | }, [currentPage, totalAuctions]); 82 | 83 | const handleRowSelection = (auction: any) => {}; 84 | 85 | return ( 86 |
87 |
88 | handleRowSelection(r.values)} 93 | /> 94 |
95 | 101 |
102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /clients/frontend/src/components/NewServiceRequests.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { Column } from "react-table"; 4 | import { Pagination } from "../common/pagination"; 5 | import { AppTable } from "../common/table"; 6 | import { ROWS_PER_TABLE_PAGE } from "../constants"; 7 | import { 8 | countRequestsByStatus, 9 | getRequestsByStatus, 10 | } from "../services/request"; 11 | import { Request } from "../types/request"; 12 | 13 | const columns: Column[] = [ 14 | { 15 | Header: "Id", 16 | accessor: "Id", 17 | }, 18 | { 19 | Header: "Title", 20 | accessor: "Title", 21 | }, 22 | { 23 | Header: "Postcode", 24 | accessor: "Postcode", 25 | }, 26 | { 27 | Header: "Info", 28 | accessor: "Info", 29 | }, 30 | { 31 | Header: "Status", 32 | accessor: "Status", 33 | }, 34 | ]; 35 | 36 | type Props = {}; 37 | 38 | const STATUS = "new"; 39 | 40 | export const NewServiceRequests: React.FC = () => { 41 | const [pageData, setPageData] = useState<{ 42 | rowData: Request[]; 43 | isLoading: boolean; 44 | totalRequests: number; 45 | }>({ 46 | rowData: [], 47 | isLoading: false, 48 | totalRequests: 0, 49 | }); 50 | const [totalRequests, setTotalRequests] = useState(0); 51 | const [currentPage, setCurrentPage] = useState(1); 52 | const navigate = useNavigate(); 53 | 54 | useEffect(() => { 55 | setPageData((prevState) => ({ 56 | ...prevState, 57 | rowData: [], 58 | isLoading: true, 59 | })); 60 | 61 | countRequestsByStatus(STATUS).then((response) => { 62 | if (response.data && response.data) { 63 | setTotalRequests(response.data); 64 | } 65 | }); 66 | 67 | getRequestsByStatus(STATUS, ROWS_PER_TABLE_PAGE, currentPage).then( 68 | (response) => { 69 | const requests = response.data || []; 70 | setPageData({ 71 | isLoading: false, 72 | rowData: requests, 73 | totalRequests: totalRequests, 74 | }); 75 | } 76 | ); 77 | }, [currentPage, totalRequests]); 78 | 79 | const handleRowSelection = (request: any) => { 80 | navigate("/update-request-status", { state: request }); 81 | }; 82 | 83 | return ( 84 |
85 |
86 | handleRowSelection(r.values)} 91 | /> 92 |
93 | 99 |
100 | ); 101 | }; 102 | -------------------------------------------------------------------------------- /pkg/messaging/kafka_test.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func logHandler1(msg Message) error { 10 | fmt.Println("logHandler1: payload:", msg.Payload) 11 | 12 | return nil 13 | } 14 | 15 | func logHandler2(msg Message) error { 16 | fmt.Println("logHandler2: payload:", msg.Payload) 17 | 18 | return nil 19 | } 20 | 21 | func logHandler3(msg Message) error { 22 | fmt.Println("logHandler3: payload:", msg.Payload) 23 | 24 | return nil 25 | } 26 | 27 | func logHandler4(msg Message) error { 28 | fmt.Println("logHandler4: payload:", msg.Payload) 29 | 30 | return nil 31 | } 32 | 33 | func logHandler5(msg Message) error { 34 | fmt.Println("logHandler5: payload:", msg.Payload) 35 | 36 | return nil 37 | } 38 | 39 | // It should publish round robin to partitions of the topic and listen rounde robin 40 | // func TestOneTopicMultipleSubscribersSameGroup(t *testing.T) { 41 | // var url = "localhost:9092" 42 | // var groupId = "testing" 43 | // var topic = "my-topic" 44 | // var publishRetries = 3 45 | 46 | // pub := NewPublisher(url, publishRetries) 47 | // defer pub.Close() 48 | 49 | // for i := 0; i < 5; i++ { 50 | // message := Message{ 51 | // Payload: i, 52 | // } 53 | 54 | // err := pub.Publish(topic, message) 55 | 56 | // if err != nil { 57 | // log.Panicln(err) 58 | // } 59 | // } 60 | 61 | // sub := NewSubscriber(url, groupId) 62 | 63 | // go sub.Subscribe(topic, logHandler1) 64 | // go sub.Subscribe(topic, logHandler2) 65 | // go sub.Subscribe(topic, logHandler3) 66 | // go sub.Subscribe(topic, logHandler4) 67 | // go sub.Subscribe(topic, logHandler5) 68 | 69 | // select {} 70 | // } 71 | 72 | // All consumers should listen to all events 73 | func TestOneTopicMultipleSubscribersDifferentGroup(t *testing.T) { 74 | var url = "localhost:9092" 75 | var groupId_1 = "testing1" 76 | var groupId_2 = "testing2" 77 | var groupId_3 = "testing3" 78 | var groupId_4 = "testing4" 79 | var groupId_5 = "testing5" 80 | var topic = "my-topic" 81 | var publishRetries = 3 82 | 83 | pub := NewPublisher(url, publishRetries) 84 | defer pub.Close() 85 | 86 | for i := 0; i < 4; i++ { 87 | message := Message{ 88 | Payload: i, 89 | } 90 | 91 | err := pub.Publish(topic, message) 92 | 93 | if err != nil { 94 | log.Panicln(err) 95 | } 96 | } 97 | 98 | sub_1 := NewSubscriber(url, groupId_1) 99 | go sub_1.Subscribe(topic, logHandler1) 100 | 101 | sub_2 := NewSubscriber(url, groupId_2) 102 | go sub_2.Subscribe(topic, logHandler2) 103 | 104 | sub_3 := NewSubscriber(url, groupId_3) 105 | go sub_3.Subscribe(topic, logHandler3) 106 | 107 | sub_4 := NewSubscriber(url, groupId_4) 108 | go sub_4.Subscribe(topic, logHandler4) 109 | 110 | sub_5 := NewSubscriber(url, groupId_5) 111 | go sub_5.Subscribe(topic, logHandler5) 112 | 113 | select {} 114 | } 115 | -------------------------------------------------------------------------------- /clients/frontend/src/components/MyAuctions.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { AppTable, Column } from "../common/table"; 3 | import { Pagination } from "../common/pagination"; 4 | import { ROWS_PER_TABLE_PAGE } from "../constants"; 5 | import { 6 | countMyAuctions, 7 | formatAuctions, 8 | getMyAuctions, 9 | } from "../services/auction"; 10 | import { FormattedAuction } from "../types/auction"; 11 | import { useNavigate } from "react-router-dom"; 12 | 13 | const columns: Column[] = [ 14 | { 15 | Header: "Id", 16 | accessor: "Id", 17 | }, 18 | { 19 | Header: "Title", 20 | accessor: "Title", 21 | }, 22 | { 23 | Header: "Postcode", 24 | accessor: "Postcode", 25 | }, 26 | { 27 | Header: "Info", 28 | accessor: "Info", 29 | }, 30 | { 31 | Header: "Deadline", 32 | accessor: "Deadline", 33 | }, 34 | { 35 | Header: "Status", 36 | accessor: "Status", 37 | }, 38 | ]; 39 | 40 | type Props = {}; 41 | 42 | export const MyAuctions: React.FC = () => { 43 | const [pageData, setPageData] = useState<{ 44 | rowData: FormattedAuction[]; 45 | isLoading: boolean; 46 | totalAuctions: number; 47 | }>({ 48 | rowData: [], 49 | isLoading: false, 50 | totalAuctions: 0, 51 | }); 52 | const [totalAuctions, setTotalAuctions] = useState(0); 53 | const [currentPage, setCurrentPage] = useState(1); 54 | const navigate = useNavigate(); 55 | 56 | useEffect(() => { 57 | setPageData((prevState) => ({ 58 | ...prevState, 59 | rowData: [], 60 | isLoading: true, 61 | })); 62 | 63 | countMyAuctions().then((response) => { 64 | if (response.data && response.data) { 65 | setTotalAuctions(response.data); 66 | } 67 | }); 68 | 69 | getMyAuctions(ROWS_PER_TABLE_PAGE, currentPage).then((response) => { 70 | const auctions = response.data || []; 71 | setPageData({ 72 | isLoading: false, 73 | rowData: formatAuctions(auctions), 74 | totalAuctions: totalAuctions, 75 | }); 76 | }); 77 | }, [currentPage, totalAuctions]); 78 | 79 | const handleRowSelection = (auction: any) => { 80 | const fullAuction = pageData.rowData.find((r) => r.Id === auction.Id); 81 | navigate("/assigned-auction", { state: fullAuction }); 82 | }; 83 | 84 | return ( 85 |
86 |
87 | handleRowSelection(r.values)} 92 | /> 93 |
94 | 100 |
101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /services/auction-service/internal/repository/bid/bid_postgres.go: -------------------------------------------------------------------------------- 1 | package bid 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/PanGan21/pkg/entity" 8 | "github.com/PanGan21/pkg/postgres" 9 | ) 10 | 11 | type bidRepository struct { 12 | db postgres.Postgres 13 | } 14 | 15 | func NewBidRepository(db postgres.Postgres) *bidRepository { 16 | return &bidRepository{db: db} 17 | } 18 | 19 | func (repo *bidRepository) Create(ctx context.Context, bid entity.Bid) error { 20 | var bidId int 21 | 22 | c, err := repo.db.Pool.Acquire(ctx) 23 | if err != nil { 24 | return err 25 | } 26 | defer c.Release() 27 | 28 | const query = ` 29 | INSERT INTO bids (Id, Amount, CreatorId, AuctionId) 30 | VALUES ($1, $2, $3, $4) RETURNING Id; 31 | ` 32 | 33 | err = c.QueryRow(ctx, query, bid.Id, bid.Amount, bid.CreatorId, bid.AuctionId).Scan(&bidId) 34 | if err != nil { 35 | return fmt.Errorf("AuctionRepo - Create - c.Exec: %w", err) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func (repo *bidRepository) FindManyByAuctionIdWithMinAmount(ctx context.Context, auctionId string) ([]entity.Bid, error) { 42 | var bids []entity.Bid 43 | 44 | c, err := repo.db.Pool.Acquire(ctx) 45 | if err != nil { 46 | return bids, err 47 | } 48 | defer c.Release() 49 | 50 | const query = ` 51 | SELECT * FROM bids WHERE AuctionId=$1 AND Amount = ( SELECT MIN(Amount) FROM bids WHERE AuctionId=$1 ); 52 | ` 53 | 54 | rows, err := c.Query(ctx, query, auctionId) 55 | if err != nil { 56 | return bids, fmt.Errorf("AuctionRepo - FindManyByAuctionIdWithMinAmount - c.Query: %w", err) 57 | } 58 | defer rows.Close() 59 | 60 | for rows.Next() { 61 | var b entity.Bid 62 | err := rows.Scan(&b.Id, &b.Amount, &b.CreatorId, &b.AuctionId) 63 | if err != nil { 64 | return bids, fmt.Errorf("AuctionRepo - FindManyByAuctionIdWithMinAmount - rows.Scan: %w", err) 65 | } 66 | bids = append(bids, b) 67 | } 68 | 69 | if err := rows.Err(); err != nil { 70 | return bids, fmt.Errorf("AuctionRepo - FindManyByAuctionIdWithMinAmount - rows.Err: %w", err) 71 | } 72 | 73 | return bids, nil 74 | } 75 | 76 | func (repo *bidRepository) FindSecondMinAmountByAuctionId(ctx context.Context, auctionId string) (float64, error) { 77 | var secondMinAmount = 0.0 78 | 79 | c, err := repo.db.Pool.Acquire(ctx) 80 | if err != nil { 81 | return secondMinAmount, err 82 | } 83 | defer c.Release() 84 | 85 | const query = ` 86 | SELECT COALESCE( 87 | (SELECT MIN(amount) 88 | FROM bids 89 | WHERE auctionId = $1 90 | AND amount <> ( 91 | SELECT MIN(amount) 92 | FROM bids 93 | WHERE auctionId = $1) 94 | ), 95 | (SELECT amount 96 | FROM bids 97 | WHERE auctionId = $1) 98 | ) AS second_best_bid; 99 | ` 100 | 101 | err = c.QueryRow(ctx, query, auctionId).Scan(&secondMinAmount) 102 | if err != nil { 103 | return secondMinAmount, fmt.Errorf("AuctionRepo - FindSecondMinAmountByAuctionId - c.Exec: %w", err) 104 | } 105 | 106 | return secondMinAmount, nil 107 | } 108 | -------------------------------------------------------------------------------- /clients/frontend/src/components/InProgressAuctions.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Column } from "react-table"; 3 | import { Pagination } from "../common/pagination"; 4 | import { AppTable } from "../common/table"; 5 | import { ROWS_PER_TABLE_PAGE } from "../constants"; 6 | import { 7 | countAuctionsByStatus, 8 | formatAuctions, 9 | getAuctionsByStatus, 10 | updateAuctionStatus, 11 | } from "../services/auction"; 12 | import { FormattedAuction } from "../types/auction"; 13 | 14 | const columns: Column[] = [ 15 | { 16 | Header: "Id", 17 | accessor: "Id", 18 | }, 19 | { 20 | Header: "Title", 21 | accessor: "Title", 22 | }, 23 | { 24 | Header: "Postcode", 25 | accessor: "Postcode", 26 | }, 27 | { 28 | Header: "Info", 29 | accessor: "Info", 30 | }, 31 | { 32 | Header: "Deadline", 33 | accessor: "Deadline", 34 | }, 35 | { 36 | Header: "Status", 37 | accessor: "Status", 38 | }, 39 | ]; 40 | 41 | const STATUS = "in progress"; 42 | 43 | const CLOSED_STATUS = "closed"; 44 | 45 | type Props = {}; 46 | 47 | export const InProgressAuctions: React.FC = () => { 48 | const [pageData, setPageData] = useState<{ 49 | rowData: FormattedAuction[]; 50 | isLoading: boolean; 51 | totalAuctions: number; 52 | }>({ 53 | rowData: [], 54 | isLoading: false, 55 | totalAuctions: 0, 56 | }); 57 | 58 | const [totalAuctions, setTotalAuctions] = useState(0); 59 | const [currentPage, setCurrentPage] = useState(1); 60 | 61 | useEffect(() => { 62 | setPageData((prevState) => ({ 63 | ...prevState, 64 | rowData: [], 65 | isLoading: true, 66 | })); 67 | 68 | countAuctionsByStatus(STATUS).then((response) => { 69 | if (response.data && response.data) { 70 | setTotalAuctions(response.data); 71 | } 72 | }); 73 | 74 | getAuctionsByStatus(STATUS, ROWS_PER_TABLE_PAGE, currentPage).then( 75 | (response) => { 76 | const auction = response.data || []; 77 | setPageData({ 78 | isLoading: false, 79 | rowData: formatAuctions(auction), 80 | totalAuctions: totalAuctions, 81 | }); 82 | } 83 | ); 84 | }, [currentPage, totalAuctions]); 85 | 86 | const handleRowSelection = (auction: any) => { 87 | updateAuctionStatus(auction.Id, CLOSED_STATUS).then((response) => { 88 | window.location.reload(); 89 | }); 90 | }; 91 | 92 | return ( 93 |
94 |
95 | handleRowSelection(r.values)} 100 | /> 101 |
102 | 108 |
109 | ); 110 | }; 111 | -------------------------------------------------------------------------------- /clients/frontend/src/components/OpenAuctions.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { AppTable, Column } from "../common/table"; 3 | import { Pagination } from "../common/pagination"; 4 | import { ROWS_PER_TABLE_PAGE } from "../constants"; 5 | import { 6 | countAuctionsByStatus, 7 | formatAuctions, 8 | getAuctionsByStatus, 9 | } from "../services/auction"; 10 | import { FormattedAuction } from "../types/auction"; 11 | import { useNavigate } from "react-router-dom"; 12 | 13 | const columns: Column[] = [ 14 | { 15 | Header: "Id", 16 | accessor: "Id", 17 | }, 18 | { 19 | Header: "Title", 20 | accessor: "Title", 21 | }, 22 | { 23 | Header: "Postcode", 24 | accessor: "Postcode", 25 | }, 26 | { 27 | Header: "Info", 28 | accessor: "Info", 29 | }, 30 | { 31 | Header: "Deadline", 32 | accessor: "Deadline", 33 | }, 34 | { 35 | Header: "Status", 36 | accessor: "Status", 37 | }, 38 | ]; 39 | 40 | type Props = {}; 41 | 42 | const STATUS = "open"; 43 | 44 | export const OpenAuctions: React.FC = () => { 45 | const [pageData, setPageData] = useState<{ 46 | rowData: FormattedAuction[]; 47 | isLoading: boolean; 48 | totalAuctions: number; 49 | }>({ 50 | rowData: [], 51 | isLoading: false, 52 | totalAuctions: 0, 53 | }); 54 | const [totalAuctions, setTotalAuctions] = useState(0); 55 | const [currentPage, setCurrentPage] = useState(1); 56 | const navigate = useNavigate(); 57 | 58 | useEffect(() => { 59 | setPageData((prevState) => ({ 60 | ...prevState, 61 | rowData: [], 62 | isLoading: true, 63 | })); 64 | 65 | countAuctionsByStatus(STATUS).then((response) => { 66 | if (response.data && response.data) { 67 | setTotalAuctions(response.data); 68 | } 69 | }); 70 | 71 | getAuctionsByStatus(STATUS, ROWS_PER_TABLE_PAGE, currentPage).then( 72 | (response) => { 73 | const auctions = response.data || []; 74 | setPageData({ 75 | isLoading: false, 76 | rowData: formatAuctions(auctions), 77 | totalAuctions: totalAuctions, 78 | }); 79 | } 80 | ); 81 | }, [currentPage, totalAuctions]); 82 | 83 | const handleRowSelection = (auction: any) => { 84 | navigate("/new-bid", { state: auction }); 85 | }; 86 | 87 | return ( 88 |
89 |
90 | Choose an auction to create a Bid! 91 |
92 |
93 |
94 |
95 | handleRowSelection(r.values)} 100 | /> 101 |
102 | 108 |
109 | ); 110 | }; 111 | -------------------------------------------------------------------------------- /clients/frontend/src/components/AssignedAuctions.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { Column } from "react-table"; 4 | import { Pagination } from "../common/pagination"; 5 | import { AppTable } from "../common/table"; 6 | import { ROWS_PER_TABLE_PAGE } from "../constants"; 7 | import { 8 | countAuctionsByStatus, 9 | formatAuctions, 10 | getAuctionsByStatus, 11 | } from "../services/auction"; 12 | import { FormattedAuction } from "../types/auction"; 13 | 14 | const columns: Column[] = [ 15 | { 16 | Header: "Id", 17 | accessor: "Id", 18 | }, 19 | { 20 | Header: "Title", 21 | accessor: "Title", 22 | }, 23 | { 24 | Header: "Postcode", 25 | accessor: "Postcode", 26 | }, 27 | { 28 | Header: "Info", 29 | accessor: "Info", 30 | }, 31 | { 32 | Header: "Deadline", 33 | accessor: "Deadline", 34 | }, 35 | { 36 | Header: "Status", 37 | accessor: "Status", 38 | }, 39 | ]; 40 | 41 | const STATUS = "assigned"; 42 | 43 | type Props = {}; 44 | 45 | export const AssignedAuctions: React.FC = () => { 46 | const [pageData, setPageData] = useState<{ 47 | rowData: FormattedAuction[]; 48 | isLoading: boolean; 49 | totalAuctions: number; 50 | }>({ 51 | rowData: [], 52 | isLoading: false, 53 | totalAuctions: 0, 54 | }); 55 | 56 | const [totalAuctions, setTotalAuctions] = useState(0); 57 | const [currentPage, setCurrentPage] = useState(1); 58 | const navigate = useNavigate(); 59 | 60 | useEffect(() => { 61 | setPageData((prevState) => ({ 62 | ...prevState, 63 | rowData: [], 64 | isLoading: true, 65 | })); 66 | 67 | countAuctionsByStatus(STATUS).then((response) => { 68 | if (response.data && response.data) { 69 | setTotalAuctions(response.data); 70 | } 71 | }); 72 | 73 | getAuctionsByStatus(STATUS, ROWS_PER_TABLE_PAGE, currentPage).then( 74 | (response) => { 75 | const auctions = response.data || []; 76 | setPageData({ 77 | isLoading: false, 78 | rowData: formatAuctions(auctions), 79 | totalAuctions: totalAuctions, 80 | }); 81 | } 82 | ); 83 | }, [currentPage, totalAuctions]); 84 | 85 | const handleRowSelection = (auction: any) => { 86 | const fullAuction = pageData.rowData.find((r) => r.Id === auction.Id); 87 | navigate("/update-auction-status", { state: fullAuction }); 88 | }; 89 | 90 | return ( 91 |
92 |
93 | handleRowSelection(r.values)} 98 | /> 99 |
100 | 106 |
107 | ); 108 | }; 109 | -------------------------------------------------------------------------------- /services/bidding-service/internal/service/bid.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | bidEvents "github.com/PanGan21/bidding-service/internal/events/bid" 8 | bidRepo "github.com/PanGan21/bidding-service/internal/repository/bid" 9 | "github.com/PanGan21/pkg/entity" 10 | "github.com/PanGan21/pkg/pagination" 11 | ) 12 | 13 | type BidService interface { 14 | Create(ctx context.Context, creatorId string, auctionId int, amount float64) (entity.Bid, error) 15 | FindOneById(ctx context.Context, id int) (entity.Bid, error) 16 | GetManyByAuctionId(ctx context.Context, auctionId int, pagination *pagination.Pagination) (*[]entity.Bid, error) 17 | GetOwn(ctx context.Context, creatorId string, pagination *pagination.Pagination) (*[]entity.Bid, error) 18 | CountOwn(ctx context.Context, creatorId string) (int, error) 19 | } 20 | 21 | type bidService struct { 22 | bidRepo bidRepo.BidRepository 23 | bidEvents bidEvents.BidEvents 24 | } 25 | 26 | func NewBidService(br bidRepo.BidRepository, be bidEvents.BidEvents) BidService { 27 | return &bidService{bidRepo: br, bidEvents: be} 28 | } 29 | 30 | func (s *bidService) Create(ctx context.Context, creatorId string, auctionId int, amount float64) (entity.Bid, error) { 31 | var newBid entity.Bid 32 | 33 | bidId, err := s.bidRepo.Create(ctx, creatorId, auctionId, amount) 34 | if err != nil { 35 | return newBid, fmt.Errorf("BidService - Create - s.bidRepo.Create: %w", err) 36 | } 37 | 38 | newBid, err = s.bidRepo.FindOneById(ctx, bidId) 39 | if err != nil { 40 | return newBid, fmt.Errorf("BidService - Create - s.bidRepo.FindOneById: %w", err) 41 | } 42 | 43 | err = s.bidEvents.PublishBidCreated(&newBid) 44 | if err != nil { 45 | return newBid, fmt.Errorf("BidService - Create - s.bidEvents.PublishBidCreated: %w", err) 46 | } 47 | 48 | return newBid, nil 49 | } 50 | 51 | func (s *bidService) FindOneById(ctx context.Context, id int) (entity.Bid, error) { 52 | var bid entity.Bid 53 | 54 | bid, err := s.bidRepo.FindOneById(ctx, id) 55 | if err != nil { 56 | return bid, fmt.Errorf("BidService - FindOneById - s.bidRepo.FindOneById: %w", err) 57 | } 58 | 59 | return bid, nil 60 | } 61 | 62 | func (s *bidService) GetManyByAuctionId(ctx context.Context, auctionId int, pagination *pagination.Pagination) (*[]entity.Bid, error) { 63 | bids, err := s.bidRepo.FindByAuctionId(ctx, auctionId, pagination) 64 | if err != nil { 65 | return nil, fmt.Errorf("BidService - GetManyByAuctionId - s.bidRepo.FindByAuctionId: %w", err) 66 | } 67 | 68 | return bids, nil 69 | } 70 | 71 | func (s *bidService) GetOwn(ctx context.Context, creatorId string, pagination *pagination.Pagination) (*[]entity.Bid, error) { 72 | bids, err := s.bidRepo.FindByCreatorId(ctx, creatorId, pagination) 73 | if err != nil { 74 | return nil, fmt.Errorf("BidService - GetOwn - s.bidRepo.FindByCreatorId: %w", err) 75 | } 76 | 77 | return bids, nil 78 | } 79 | 80 | func (s *bidService) CountOwn(ctx context.Context, creatorId string) (int, error) { 81 | count, err := s.bidRepo.CountByCreatorId(ctx, creatorId) 82 | if err != nil { 83 | return 0, fmt.Errorf("BidService - CountOwn - s.bidRepo.CountByCreatorId: %w", err) 84 | } 85 | 86 | return count, nil 87 | } 88 | -------------------------------------------------------------------------------- /clients/frontend/src/components/PendingAuctions.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { Column } from "react-table"; 4 | import { Pagination } from "../common/pagination"; 5 | import { AppTable } from "../common/table"; 6 | import { ROWS_PER_TABLE_PAGE } from "../constants"; 7 | import { 8 | countOpenPastDeadlineAuctions, 9 | formatExtendedAuctions, 10 | getOpenPastDeadlineAuctions, 11 | } from "../services/auction"; 12 | import { ExtendedFormattedAuction } from "../types/auction"; 13 | 14 | const columns: Column[] = [ 15 | { 16 | Header: "Id", 17 | accessor: "Id", 18 | }, 19 | { 20 | Header: "Title", 21 | accessor: "Title", 22 | }, 23 | { 24 | Header: "Postcode", 25 | accessor: "Postcode", 26 | }, 27 | { 28 | Header: "Info", 29 | accessor: "Info", 30 | }, 31 | { 32 | Header: "Deadline", 33 | accessor: "Deadline", 34 | }, 35 | { 36 | Header: "Status", 37 | accessor: "Status", 38 | }, 39 | { 40 | Header: "# Bids", 41 | accessor: "BidsCount", 42 | }, 43 | ]; 44 | 45 | type Props = {}; 46 | 47 | export const PendingAuctions: React.FC = () => { 48 | const [pageData, setPageData] = useState<{ 49 | rowData: ExtendedFormattedAuction[]; 50 | isLoading: boolean; 51 | totalAuctions: number; 52 | }>({ 53 | rowData: [], 54 | isLoading: false, 55 | totalAuctions: 0, 56 | }); 57 | const [totalAuctions, setTotalAuctions] = useState(0); 58 | const [currentPage, setCurrentPage] = useState(1); 59 | const navigate = useNavigate(); 60 | 61 | useEffect(() => { 62 | setPageData((prevState) => ({ 63 | ...prevState, 64 | rowData: [], 65 | isLoading: true, 66 | })); 67 | 68 | countOpenPastDeadlineAuctions().then((response) => { 69 | if (response.data) { 70 | setTotalAuctions(response.data); 71 | } 72 | }); 73 | 74 | getOpenPastDeadlineAuctions(ROWS_PER_TABLE_PAGE, currentPage).then( 75 | (response) => { 76 | const auctions = response.data || []; 77 | setPageData({ 78 | isLoading: false, 79 | rowData: formatExtendedAuctions(auctions), 80 | totalAuctions: totalAuctions, 81 | }); 82 | } 83 | ); 84 | }, [currentPage, totalAuctions]); 85 | 86 | const handleRowSelection = (auction: any) => { 87 | const fullAuction = pageData.rowData.find((r) => r.Id === auction.Id); 88 | navigate("/update-pending-auction", { state: fullAuction }); 89 | }; 90 | 91 | return ( 92 |
93 |
94 | handleRowSelection(r.values)} 99 | /> 100 |
101 | 107 |
108 | ); 109 | }; 110 | -------------------------------------------------------------------------------- /clients/frontend/src/components/Assignments.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | import { Pagination } from "../common/pagination"; 4 | import { AppTable, Column } from "../common/table"; 5 | import { ROWS_PER_TABLE_PAGE } from "../constants"; 6 | import { countOwnAssignments, getOwnAssignments } from "../services/auction"; 7 | import { Auction } from "../types/auction"; 8 | import { User } from "../types/user"; 9 | 10 | const columns: Column[] = [ 11 | { 12 | Header: "Id", 13 | accessor: "Id", 14 | }, 15 | { 16 | Header: "Title", 17 | accessor: "Title", 18 | }, 19 | { 20 | Header: "CreatorId", 21 | accessor: "CreatorId", 22 | }, 23 | { 24 | Header: "Postcode", 25 | accessor: "Postcode", 26 | }, 27 | { 28 | Header: "Info", 29 | accessor: "Info", 30 | }, 31 | { 32 | Header: "Status", 33 | accessor: "Status", 34 | }, 35 | { 36 | Header: "WinningBidId", 37 | accessor: "WinningBidId", 38 | }, 39 | { 40 | Header: "WinningAmount", 41 | accessor: "WinningAmount", 42 | }, 43 | ]; 44 | 45 | type Props = {}; 46 | 47 | const handleRowSelection = (auction: any) => {}; 48 | 49 | export const Assignments: React.FC = () => { 50 | const [pageData, setPageData] = useState<{ 51 | rowData: Auction[]; 52 | isLoading: boolean; 53 | totalAssignments: number; 54 | }>({ 55 | rowData: [], 56 | isLoading: false, 57 | totalAssignments: 0, 58 | }); 59 | const [totalAssignments, setTotalAssignments] = useState(0); 60 | const [currentPage, setCurrentPage] = useState(1); 61 | const { state }: { state: User } = useLocation(); 62 | 63 | useEffect(() => { 64 | setPageData((prevState) => ({ 65 | ...prevState, 66 | rowData: [], 67 | isLoading: true, 68 | })); 69 | 70 | countOwnAssignments().then((response) => { 71 | if (response.data && response.data) { 72 | setTotalAssignments(response.data); 73 | } 74 | }); 75 | 76 | getOwnAssignments(ROWS_PER_TABLE_PAGE, currentPage).then((response) => { 77 | const assignments = response.data || []; 78 | setPageData({ 79 | isLoading: false, 80 | rowData: assignments, 81 | totalAssignments, 82 | }); 83 | }); 84 | }, [currentPage, totalAssignments]); 85 | 86 | return ( 87 |
88 |
89 |

90 | The following is a list of auctions assigned to{" "} 91 | {state.Username !== "" ? state.Username : "you"} 92 |

93 |
94 |
95 |
96 | handleRowSelection(r.values)} 101 | /> 102 |
103 | 109 |
110 | ); 111 | }; 112 | -------------------------------------------------------------------------------- /clients/frontend/src/components/AdminBoard.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import auction from "../assets/auction.png"; 3 | import { AssignedAuctions } from "./AssignedAuctions"; 4 | import { ClosedAuctions } from "./ClosedAuctions"; 5 | import { InProgressAuctions } from "./InProgressAuctions"; 6 | import { PendingAuctions } from "./PendingAuctions"; 7 | 8 | type Props = {}; 9 | 10 | export const AdminBoard: React.FC = () => { 11 | const [isPendingAuctionsOpen, setPendingAuctionsOpen] = useState(false); 12 | const [isAssignedAuctionsOpen, setAssignedOpen] = useState(false); 13 | const [isInProgressAuctionsOpen, setInProgressOpen] = useState(false); 14 | const [isClosedAuctionsOpen, setClosedOpen] = useState(false); 15 | 16 | const togglePendingAuctions = () => { 17 | setPendingAuctionsOpen(!isPendingAuctionsOpen); 18 | }; 19 | 20 | const toggleAssigned = () => { 21 | setAssignedOpen(!isAssignedAuctionsOpen); 22 | }; 23 | 24 | const toggleInProgress = () => { 25 | setInProgressOpen(!isInProgressAuctionsOpen); 26 | }; 27 | 28 | const toggleClosed = () => { 29 | setClosedOpen(!isClosedAuctionsOpen); 30 | }; 31 | 32 | return ( 33 |
34 |
35 | profile-img 40 | 41 | Pending Auctions 42 | 43 |
44 |
45 | Choose an auction to resolve the winning bid! 46 | 47 |
48 |
49 |
50 | profile-img 55 | 56 | Assigned Auctions 57 | 58 |
59 |
60 | Choose an auction to update the status! 61 | 62 |
63 |
64 |
65 | profile-img 70 | 71 | In Progress Auctions 72 | 73 |
74 |
75 | Choose an auction to close it! 76 | 77 |
78 |
79 |
80 | profile-img 85 | 86 | Closed Auctions 87 | 88 |
89 |
90 | All the closed auction from residents! 91 | 92 |
93 |
94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /integration-test/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/integration-test 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/Eun/go-hit v0.5.23 7 | github.com/PanGan21/pkg/auth v0.0.0-00010101000000-000000000000 8 | github.com/PanGan21/pkg/entity v0.0.0-00010101000000-000000000000 9 | github.com/PanGan21/pkg/postgres v0.0.0-00010101000000-000000000000 10 | github.com/google/uuid v1.3.0 11 | ) 12 | 13 | require ( 14 | github.com/Eun/go-convert v1.2.12 // indirect 15 | github.com/Eun/go-doppelgangerreader v0.0.0-20220728163552-459d94705224 // indirect 16 | github.com/Masterminds/squirrel v1.5.3 // indirect 17 | github.com/PanGan21/pkg/utils v0.0.0-00010101000000-000000000000 // indirect 18 | github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect 19 | github.com/gin-contrib/sse v0.1.0 // indirect 20 | github.com/gin-gonic/gin v1.8.1 // indirect 21 | github.com/go-playground/locales v0.14.0 // indirect 22 | github.com/go-playground/universal-translator v0.18.0 // indirect 23 | github.com/go-playground/validator/v10 v10.10.0 // indirect 24 | github.com/goccy/go-json v0.9.7 // indirect 25 | github.com/gofrs/uuid v4.3.1+incompatible // indirect 26 | github.com/golang-jwt/jwt/v4 v4.4.2 // indirect 27 | github.com/google/go-cmp v0.5.9 // indirect 28 | github.com/gookit/color v1.5.2 // indirect 29 | github.com/itchyny/gojq v0.12.9 // indirect 30 | github.com/itchyny/timefmt-go v0.1.4 // indirect 31 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 32 | github.com/jackc/pgconn v1.13.0 // indirect 33 | github.com/jackc/pgio v1.0.0 // indirect 34 | github.com/jackc/pgpassfile v1.0.0 // indirect 35 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 36 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 37 | github.com/jackc/pgtype v1.12.0 // indirect 38 | github.com/jackc/pgx/v4 v4.17.2 // indirect 39 | github.com/jackc/puddle v1.3.0 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/k0kubun/pp v3.0.1+incompatible // indirect 42 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 43 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 44 | github.com/leodido/go-urn v1.2.1 // indirect 45 | github.com/lunixbochs/vtclean v1.0.0 // indirect 46 | github.com/mattn/go-colorable v0.1.13 // indirect 47 | github.com/mattn/go-isatty v0.0.16 // indirect 48 | github.com/mitchellh/mapstructure v1.5.0 // indirect 49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 50 | github.com/modern-go/reflect2 v1.0.2 // indirect 51 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 52 | github.com/tidwall/pretty v1.2.1 // indirect 53 | github.com/ugorji/go/codec v1.2.7 // indirect 54 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 55 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 56 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect 57 | golang.org/x/sys v0.2.0 // indirect 58 | golang.org/x/text v0.3.7 // indirect 59 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 60 | google.golang.org/protobuf v1.28.0 // indirect 61 | gopkg.in/yaml.v2 v2.4.0 // indirect 62 | ) 63 | 64 | replace ( 65 | github.com/PanGan21/pkg/auth => ../pkg/auth 66 | github.com/PanGan21/pkg/entity => ../pkg/entity 67 | github.com/PanGan21/pkg/postgres => ../pkg/postgres 68 | github.com/PanGan21/pkg/utils => ../pkg/utils 69 | ) 70 | -------------------------------------------------------------------------------- /clients/frontend/src/components/CreateBid.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { NavigateFunction, useLocation, useNavigate } from "react-router-dom"; 3 | import { FormattedAuction } from "../types/auction"; 4 | import * as Yup from "yup"; 5 | import { NewBid } from "../types/bid"; 6 | import { Formik, Field, Form, ErrorMessage } from "formik"; 7 | import bid from "../assets/bid.png"; 8 | import { createBid } from "../services/bid"; 9 | 10 | type Props = {}; 11 | 12 | export const CreateBid: React.FC = () => { 13 | const navigate: NavigateFunction = useNavigate(); 14 | const { state }: { state: FormattedAuction } = useLocation(); 15 | 16 | const [loading, setLoading] = useState(false); 17 | const [message, setMessage] = useState(""); 18 | 19 | const initialValues: { 20 | amount: number; 21 | requstId: string; 22 | } = { 23 | amount: 0, 24 | requstId: "", 25 | }; 26 | 27 | const validationSchema = Yup.object().shape({ 28 | amount: Yup.number().moreThan(0, "The number must be greater than 0"), 29 | }); 30 | 31 | const handleSubmit = async (formValue: { amount: number }) => { 32 | const { amount } = formValue; 33 | 34 | const newBid: NewBid = { 35 | Amount: amount, 36 | AuctionId: state.Id, 37 | }; 38 | 39 | setMessage(""); 40 | setLoading(true); 41 | 42 | try { 43 | await createBid(newBid); 44 | navigate("/open-auctions"); 45 | window.location.reload(); 46 | } catch (error: any) { 47 | const resMessage = 48 | (error.response && 49 | error.response.data && 50 | error.response.data.message) || 51 | error.message || 52 | error.toString(); 53 | 54 | setLoading(false); 55 | setMessage(resMessage); 56 | } 57 | }; 58 | 59 | return ( 60 |
61 |
62 | profile-img 63 | 68 |
69 |
70 | 73 | 74 | 79 |
80 | 81 |
82 | 92 |
93 | 94 | {message && ( 95 |
96 |
97 | {message} 98 |
99 |
100 | )} 101 |
102 |
103 |
104 |
105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /pkg/entity/auction.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type Auction struct { 8 | Id int `json:"Id" db:"Id"` 9 | Title string `json:"Title" db:"Title"` 10 | Postcode string `json:"Postcode" db:"Postcode"` 11 | Info string `json:"Info" db:"Info"` 12 | CreatorId string `json:"CreatorId" db:"CreatorId"` 13 | Deadline int64 `json:"Deadline" db:"Deadline"` 14 | Status AuctionStatus `json:"Status" db:"Status"` 15 | WinningBidId string `json:"WinningBidId" db:"WinningBidId"` 16 | WinnerId string `json:"WinnerId" db:"WinnerId"` 17 | WinningAmount float64 `json:"WinningAmount" db:"WinningAmount"` 18 | } 19 | 20 | type ExtendedAuction struct { 21 | Id int `json:"Id" db:"Id"` 22 | Title string `json:"Title" db:"Title"` 23 | Postcode string `json:"Postcode" db:"Postcode"` 24 | Info string `json:"Info" db:"Info"` 25 | CreatorId string `json:"CreatorId" db:"CreatorId"` 26 | Deadline int64 `json:"Deadline" db:"Deadline"` 27 | Status AuctionStatus `json:"Status" db:"Status"` 28 | WinningBidId string `json:"WinningBidId" db:"WinningBidId"` 29 | BidsCount int `json:"BidsCount" db:"BidsCount"` 30 | } 31 | 32 | type AuctionStatus string 33 | 34 | const ( 35 | Open AuctionStatus = "open" 36 | Assigned AuctionStatus = "assigned" 37 | InProgress AuctionStatus = "in progress" 38 | Closed AuctionStatus = "closed" 39 | ) 40 | 41 | var ErrIncorrectAuctionType = errors.New("incorrect auction type") 42 | 43 | func IsAuctionType(unknown interface{}) (Auction, error) { 44 | var auction Auction 45 | 46 | unknownMap, ok := unknown.(map[string]interface{}) 47 | if !ok { 48 | return auction, ErrIncorrectAuctionType 49 | } 50 | 51 | auction.CreatorId, ok = unknownMap["CreatorId"].(string) 52 | if !ok { 53 | return auction, ErrIncorrectAuctionType 54 | } 55 | 56 | auction.Info, ok = unknownMap["Info"].(string) 57 | if !ok { 58 | return auction, ErrIncorrectAuctionType 59 | } 60 | 61 | auction.Postcode, ok = unknownMap["Postcode"].(string) 62 | if !ok { 63 | return auction, ErrIncorrectAuctionType 64 | } 65 | 66 | auction.Title, ok = unknownMap["Title"].(string) 67 | if !ok { 68 | return auction, ErrIncorrectAuctionType 69 | } 70 | 71 | floatId, ok := unknownMap["Id"].(float64) 72 | if !ok { 73 | return auction, ErrIncorrectAuctionType 74 | } 75 | auction.Id = int(floatId) 76 | 77 | floatDeadline, ok := unknownMap["Deadline"].(float64) 78 | if !ok { 79 | return auction, ErrIncorrectAuctionType 80 | } 81 | auction.Deadline = int64(floatDeadline) 82 | 83 | s, ok := unknownMap["Status"].(string) 84 | if !ok { 85 | return auction, ErrIncorrectAuctionType 86 | } 87 | status := AuctionStatus(s) 88 | 89 | switch status { 90 | case Open, Assigned, InProgress, Closed: 91 | auction.Status = status 92 | default: 93 | return auction, ErrIncorrectAuctionType 94 | } 95 | 96 | auction.WinningBidId = unknownMap["WinningBidId"].(string) 97 | if !ok { 98 | return auction, ErrIncorrectAuctionType 99 | } 100 | 101 | auction.WinnerId = unknownMap["WinnerId"].(string) 102 | if !ok { 103 | return auction, ErrIncorrectAuctionType 104 | } 105 | 106 | auction.WinningAmount, ok = unknownMap["WinningAmount"].(float64) 107 | if !ok { 108 | return auction, ErrIncorrectAuctionType 109 | } 110 | 111 | return auction, nil 112 | } 113 | -------------------------------------------------------------------------------- /services/auction-service/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/auction-service 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/PanGan21/pkg/auth v0.0.0-00010101000000-000000000000 7 | github.com/PanGan21/pkg/entity v0.0.0-00010101000000-000000000000 8 | github.com/PanGan21/pkg/httpserver v0.0.0-00010101000000-000000000000 9 | github.com/PanGan21/pkg/logger v0.0.0-00010101000000-000000000000 10 | github.com/PanGan21/pkg/messaging v0.0.0-00010101000000-000000000000 11 | github.com/PanGan21/pkg/pagination v0.0.0-00010101000000-000000000000 12 | github.com/PanGan21/pkg/postgres v0.0.0-00010101000000-000000000000 13 | github.com/PanGan21/pkg/utils v0.0.0-00010101000000-000000000000 14 | github.com/gin-contrib/cors v1.4.0 15 | github.com/gin-gonic/gin v1.8.1 16 | github.com/ilyakaznacheev/cleanenv v1.4.0 17 | ) 18 | 19 | require ( 20 | github.com/BurntSushi/toml v1.1.0 // indirect 21 | github.com/Masterminds/squirrel v1.5.3 // indirect 22 | github.com/gin-contrib/sse v0.1.0 // indirect 23 | github.com/go-playground/locales v0.14.0 // indirect 24 | github.com/go-playground/universal-translator v0.18.0 // indirect 25 | github.com/go-playground/validator/v10 v10.10.0 // indirect 26 | github.com/goccy/go-json v0.9.7 // indirect 27 | github.com/golang-jwt/jwt/v4 v4.4.2 // indirect 28 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 29 | github.com/jackc/pgconn v1.13.0 // indirect 30 | github.com/jackc/pgio v1.0.0 // indirect 31 | github.com/jackc/pgpassfile v1.0.0 // indirect 32 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 33 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 34 | github.com/jackc/pgtype v1.12.0 // indirect 35 | github.com/jackc/pgx/v4 v4.17.2 // indirect 36 | github.com/jackc/puddle v1.3.0 // indirect 37 | github.com/joho/godotenv v1.4.0 // indirect 38 | github.com/json-iterator/go v1.1.12 // indirect 39 | github.com/klauspost/compress v1.15.13 // indirect 40 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 41 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 42 | github.com/leodido/go-urn v1.2.1 // indirect 43 | github.com/mattn/go-colorable v0.1.12 // indirect 44 | github.com/mattn/go-isatty v0.0.14 // indirect 45 | github.com/mitchellh/mapstructure v1.5.0 // indirect 46 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 47 | github.com/modern-go/reflect2 v1.0.2 // indirect 48 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 49 | github.com/pierrec/lz4/v4 v4.1.17 // indirect 50 | github.com/rs/zerolog v1.28.0 // indirect 51 | github.com/segmentio/kafka-go v0.4.38 // indirect 52 | github.com/ugorji/go/codec v1.2.7 // indirect 53 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 54 | golang.org/x/net v0.0.0-20220927171203-f486391704dc // indirect 55 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect 56 | golang.org/x/text v0.3.7 // indirect 57 | google.golang.org/protobuf v1.28.0 // indirect 58 | gopkg.in/yaml.v2 v2.4.0 // indirect 59 | gopkg.in/yaml.v3 v3.0.1 // indirect 60 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect 61 | ) 62 | 63 | replace ( 64 | github.com/PanGan21/pkg/auth => ../../pkg/auth 65 | github.com/PanGan21/pkg/entity => ../../pkg/entity 66 | github.com/PanGan21/pkg/httpserver => ../../pkg/httpserver 67 | github.com/PanGan21/pkg/logger => ../../pkg/logger 68 | github.com/PanGan21/pkg/messaging => ../../pkg/messaging 69 | github.com/PanGan21/pkg/pagination => ../../pkg/pagination 70 | github.com/PanGan21/pkg/postgres => ../../pkg/postgres 71 | github.com/PanGan21/pkg/utils => ../../pkg/utils 72 | ) 73 | -------------------------------------------------------------------------------- /services/bidding-service/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/bidding-service 2 | 3 | go 1.20 4 | 5 | replace ( 6 | github.com/PanGan21/pkg/auth => ../../pkg/auth 7 | github.com/PanGan21/pkg/entity => ../../pkg/entity 8 | github.com/PanGan21/pkg/httpserver => ../../pkg/httpserver 9 | github.com/PanGan21/pkg/logger => ../../pkg/logger 10 | github.com/PanGan21/pkg/messaging => ../../pkg/messaging 11 | github.com/PanGan21/pkg/pagination => ../../pkg/pagination 12 | github.com/PanGan21/pkg/postgres => ../../pkg/postgres 13 | github.com/PanGan21/pkg/utils => ../../pkg/utils 14 | ) 15 | 16 | require ( 17 | github.com/PanGan21/pkg/auth v0.0.0-00010101000000-000000000000 18 | github.com/PanGan21/pkg/entity v0.0.0-00010101000000-000000000000 19 | github.com/PanGan21/pkg/httpserver v0.0.0-00010101000000-000000000000 20 | github.com/PanGan21/pkg/logger v0.0.0-00010101000000-000000000000 21 | github.com/PanGan21/pkg/messaging v0.0.0-00010101000000-000000000000 22 | github.com/PanGan21/pkg/pagination v0.0.0-00010101000000-000000000000 23 | github.com/PanGan21/pkg/postgres v0.0.0-00010101000000-000000000000 24 | github.com/gin-contrib/cors v1.4.0 25 | github.com/gin-gonic/gin v1.8.1 26 | github.com/ilyakaznacheev/cleanenv v1.4.1 27 | ) 28 | 29 | require ( 30 | github.com/BurntSushi/toml v1.1.0 // indirect 31 | github.com/Masterminds/squirrel v1.5.3 // indirect 32 | github.com/PanGan21/pkg/utils v0.0.0-00010101000000-000000000000 // indirect 33 | github.com/gin-contrib/sse v0.1.0 // indirect 34 | github.com/go-playground/locales v0.14.0 // indirect 35 | github.com/go-playground/universal-translator v0.18.0 // indirect 36 | github.com/go-playground/validator/v10 v10.10.0 // indirect 37 | github.com/goccy/go-json v0.9.7 // indirect 38 | github.com/golang-jwt/jwt/v4 v4.4.2 // indirect 39 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 40 | github.com/jackc/pgconn v1.13.0 // indirect 41 | github.com/jackc/pgio v1.0.0 // indirect 42 | github.com/jackc/pgpassfile v1.0.0 // indirect 43 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 44 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 45 | github.com/jackc/pgtype v1.12.0 // indirect 46 | github.com/jackc/pgx/v4 v4.17.2 // indirect 47 | github.com/jackc/puddle v1.3.0 // indirect 48 | github.com/joho/godotenv v1.4.0 // indirect 49 | github.com/json-iterator/go v1.1.12 // indirect 50 | github.com/klauspost/compress v1.15.13 // indirect 51 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 52 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 53 | github.com/leodido/go-urn v1.2.1 // indirect 54 | github.com/mattn/go-colorable v0.1.12 // indirect 55 | github.com/mattn/go-isatty v0.0.14 // indirect 56 | github.com/mitchellh/mapstructure v1.5.0 // indirect 57 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 58 | github.com/modern-go/reflect2 v1.0.2 // indirect 59 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 60 | github.com/pierrec/lz4/v4 v4.1.17 // indirect 61 | github.com/rs/zerolog v1.28.0 // indirect 62 | github.com/segmentio/kafka-go v0.4.38 // indirect 63 | github.com/ugorji/go/codec v1.2.7 // indirect 64 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 65 | golang.org/x/net v0.0.0-20220927171203-f486391704dc // indirect 66 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect 67 | golang.org/x/text v0.3.7 // indirect 68 | google.golang.org/protobuf v1.28.0 // indirect 69 | gopkg.in/yaml.v2 v2.4.0 // indirect 70 | gopkg.in/yaml.v3 v3.0.1 // indirect 71 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect 72 | ) 73 | -------------------------------------------------------------------------------- /clients/frontend/src/common/pagination/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import styles from "./styles.module.css"; 3 | 4 | export const Pagination = ({ 5 | pageChangeHandler, 6 | totalRows, 7 | rowsPerPage, 8 | currentPage, 9 | }: { 10 | pageChangeHandler: Function; 11 | totalRows: number; 12 | rowsPerPage: number; 13 | currentPage: number; 14 | }) => { 15 | // Calculating max number of pages 16 | const noOfPages = Math.ceil(totalRows / rowsPerPage); 17 | 18 | // Creating an array with length equal to no.of pages 19 | const pagesArr = [...new Array(noOfPages)]; 20 | 21 | // Navigation arrows enable/disable state 22 | const [canGoBack, setCanGoBack] = useState(false); 23 | const [canGoNext, setCanGoNext] = useState(true); 24 | 25 | // These variables give the first and last record/row number 26 | // with respect to the current page 27 | const [pageFirstRecord, setPageFirstRecord] = useState(1); 28 | const [pageLastRecord, setPageLastRecord] = useState(rowsPerPage); 29 | 30 | // Onclick handlers for the butons 31 | const onNextPage = () => pageChangeHandler(currentPage + 1); 32 | const onPrevPage = () => pageChangeHandler(currentPage - 1); 33 | const onPageSelect = (pageNo: number) => pageChangeHandler(pageNo); 34 | 35 | // Disable previous and next buttons in the first and last page 36 | // respectively 37 | useEffect(() => { 38 | if (noOfPages === currentPage) { 39 | setCanGoNext(false); 40 | } else { 41 | setCanGoNext(true); 42 | } 43 | if (currentPage === 1) { 44 | setCanGoBack(false); 45 | } else { 46 | setCanGoBack(true); 47 | } 48 | }, [noOfPages, currentPage]); 49 | 50 | // To set the starting index of the page 51 | useEffect(() => { 52 | const skipFactor = (currentPage - 1) * rowsPerPage; 53 | // Some APIs require skip for paginaiton. If needed use that instead 54 | // pageChangeHandler(skipFactor); 55 | // pageChangeHandler(currentPage) 56 | setPageFirstRecord(skipFactor + 1); 57 | }, [currentPage, rowsPerPage]); 58 | 59 | // To set the last index of the page 60 | useEffect(() => { 61 | const count = pageFirstRecord + rowsPerPage; 62 | setPageLastRecord(count > totalRows ? totalRows : count - 1); 63 | }, [pageFirstRecord, rowsPerPage, totalRows]); 64 | 65 | return ( 66 | <> 67 | {noOfPages > 1 ? ( 68 |
69 |
70 | Showing {pageFirstRecord} - {pageLastRecord} of {totalRows} 71 |
72 |
73 | 80 | {pagesArr.map((num, index) => ( 81 | 90 | ))} 91 | 98 |
99 |
100 | ) : null} 101 | 102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /services/user-service/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PanGan21/user-service 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/PanGan21/pkg/auth v0.0.0-00010101000000-000000000000 7 | github.com/PanGan21/pkg/entity v0.0.0-00010101000000-000000000000 8 | github.com/PanGan21/pkg/httpserver v0.0.0-00010101000000-000000000000 9 | github.com/PanGan21/pkg/logger v0.0.0-00010101000000-000000000000 10 | github.com/PanGan21/pkg/postgres v0.0.0-00010101000000-000000000000 11 | github.com/gin-contrib/cors v1.4.0 12 | github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 13 | github.com/gin-gonic/gin v1.8.1 14 | github.com/ilyakaznacheev/cleanenv v1.3.0 15 | ) 16 | 17 | require ( 18 | github.com/BurntSushi/toml v1.2.0 // indirect 19 | github.com/Masterminds/squirrel v1.5.3 // indirect 20 | github.com/PanGan21/pkg/utils v0.0.0-00010101000000-000000000000 // indirect 21 | github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect 22 | github.com/gin-contrib/sse v0.1.0 // indirect 23 | github.com/go-playground/locales v0.14.0 // indirect 24 | github.com/go-playground/universal-translator v0.18.0 // indirect 25 | github.com/go-playground/validator/v10 v10.11.1 // indirect 26 | github.com/goccy/go-json v0.9.11 // indirect 27 | github.com/gofrs/uuid v4.3.0+incompatible // indirect 28 | github.com/golang-jwt/jwt/v4 v4.4.2 // indirect 29 | github.com/gomodule/redigo v2.0.0+incompatible // indirect 30 | github.com/google/go-cmp v0.5.9 // indirect 31 | github.com/gorilla/context v1.1.1 // indirect 32 | github.com/gorilla/securecookie v1.1.1 // indirect 33 | github.com/gorilla/sessions v1.2.1 // indirect 34 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 35 | github.com/jackc/pgconn v1.13.0 // indirect 36 | github.com/jackc/pgio v1.0.0 // indirect 37 | github.com/jackc/pgpassfile v1.0.0 // indirect 38 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 39 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 40 | github.com/jackc/pgtype v1.12.0 // indirect 41 | github.com/jackc/pgx/v4 v4.17.2 // indirect 42 | github.com/jackc/puddle v1.3.0 // indirect 43 | github.com/joho/godotenv v1.4.0 // indirect 44 | github.com/json-iterator/go v1.1.12 // indirect 45 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 46 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 47 | github.com/leodido/go-urn v1.2.1 // indirect 48 | github.com/lib/pq v1.10.7 // indirect 49 | github.com/mattn/go-colorable v0.1.13 // indirect 50 | github.com/mattn/go-isatty v0.0.16 // indirect 51 | github.com/mitchellh/mapstructure v1.5.0 // indirect 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 53 | github.com/modern-go/reflect2 v1.0.2 // indirect 54 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 55 | github.com/rs/zerolog v1.28.0 // indirect 56 | github.com/ugorji/go/codec v1.2.7 // indirect 57 | golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect 58 | golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect 59 | golang.org/x/sys v0.1.0 // indirect 60 | golang.org/x/text v0.3.7 // indirect 61 | google.golang.org/protobuf v1.28.1 // indirect 62 | gopkg.in/yaml.v2 v2.4.0 // indirect 63 | gopkg.in/yaml.v3 v3.0.1 // indirect 64 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect 65 | ) 66 | 67 | replace ( 68 | github.com/PanGan21/pkg/auth => ../../pkg/auth 69 | github.com/PanGan21/pkg/entity => ../../pkg/entity 70 | github.com/PanGan21/pkg/httpserver => ../../pkg/httpserver 71 | github.com/PanGan21/pkg/logger => ../../pkg/logger 72 | github.com/PanGan21/pkg/postgres => ../../pkg/postgres 73 | github.com/PanGan21/pkg/utils => ../../pkg/utils 74 | ) 75 | --------------------------------------------------------------------------------