diff --git a/.gitignore b/.gitignore index 64ae7f3..fec6d12 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,14 @@ go.work go.work.sum +# Binary dir +.build/ + # env file -.env \ No newline at end of file +.env + +.run + +# temporary +coworker/ +webapp/ \ No newline at end of file diff --git a/dockerfile b/dockerfile index e1d6c8a..daf5c37 100644 --- a/dockerfile +++ b/dockerfile @@ -8,6 +8,7 @@ RUN go mod download && go mod verify EXPOSE 8080 COPY . . -RUN go build -v -o /usr/local/bin/app ./... +RUN go build -v -o ./app . +RUN chmod +x ./app -CMD ["app"] \ No newline at end of file +CMD ["./app", "-c", "./misc/config.yaml"] \ No newline at end of file diff --git a/main.go b/main.go index 1c7d398..40ac6e1 100644 --- a/main.go +++ b/main.go @@ -1,15 +1,16 @@ package main import ( - "backend/args_parser" - "backend/config" - "backend/logger" - "backend/src/handlers" - "backend/src/middleware" - "backend/src/models" - "backend/src/repo" - "backend/src/services" - "backend/src/utils" + "backend/src/args_parser" + "backend/src/client_notifier" + "backend/src/config" + "backend/src/core/models" + "backend/src/core/repos" + "backend/src/core/services" + "backend/src/core/utils" + "backend/src/logger" + "backend/src/server/handlers" + "backend/src/server/middleware" "crypto/rsa" "crypto/x509" "database/sql" @@ -75,10 +76,12 @@ func main() { jwtUtil := utils.NewJwtUtil(key) passwordUtil := utils.NewPasswordUtil() - userRepo := repo.NewUserRepo(sqlDb) - userCache := repo.NewCacheInmem[string, models.UserDTO](60 * 60) - emailRepo := repo.NewEmailRepo() - actionTokenRepo := repo.NewActionTokenRepo(sqlDb) + userRepo := repos.NewUserRepo(sqlDb) + userCache := repos.NewCacheInmem[string, models.UserDTO](60 * 60) + emailRepo := repos.NewEmailRepo() + actionTokenRepo := repos.NewActionTokenRepo(sqlDb) + + clientNotifier := client_notifier.NewBasicNotifier() userService := services.NewUserService( services.UserServiceDeps{ @@ -92,7 +95,7 @@ func main() { ) linkService := services.NewShortlinkSevice( services.NewShortlinkServiceParams{ - Cache: repo.NewCacheInmem[string, string](7 * 24 * 60 * 60), + Cache: repos.NewCacheInmem[string, string](7 * 24 * 60 * 60), }, ) @@ -116,6 +119,9 @@ func main() { dummyGroup.Use(middleware.NewAuthMiddleware(userService)) dummyGroup.GET("/", handlers.NewDummyHandler()) + lpGroup := r.Group("/pooling") + lpGroup.GET("/", handlers.NewLongPoolingHandler(clientNotifier)) + listenAddr := fmt.Sprintf(":%d", conf.GetPort()) logger.Log().Msgf("server listening on %s", listenAddr) diff --git a/makefile b/makefile new file mode 100644 index 0000000..0b351d6 --- /dev/null +++ b/makefile @@ -0,0 +1,4 @@ +all: release + +release: + go build -o ./.build/release/backend main.go \ No newline at end of file diff --git a/config_example/config.yaml b/misc/config.yaml similarity index 100% rename from config_example/config.yaml rename to misc/config.yaml diff --git a/config_example/jwt_signing_key b/misc/jwt_signing_key similarity index 100% rename from config_example/jwt_signing_key rename to misc/jwt_signing_key diff --git a/db_init.sql b/sql/db_init.sql similarity index 100% rename from db_init.sql rename to sql/db_init.sql diff --git a/args_parser/args.go b/src/args_parser/args.go similarity index 100% rename from args_parser/args.go rename to src/args_parser/args.go diff --git a/src/client_notifier/event.go b/src/client_notifier/event.go new file mode 100644 index 0000000..f577329 --- /dev/null +++ b/src/client_notifier/event.go @@ -0,0 +1,12 @@ +package client_notifier + +type Event struct { + Type EventType + Data []byte +} + +type EventType string + +const ( + EventTypeEmailConfirmed EventType = "event_email_confirmed" +) diff --git a/src/client_notifier/notifier.go b/src/client_notifier/notifier.go new file mode 100644 index 0000000..cd03efc --- /dev/null +++ b/src/client_notifier/notifier.go @@ -0,0 +1,57 @@ +package client_notifier + +import "sync" + +type ClientNotifier interface { + RegisterClient(id string) <-chan Event + UnregisterClient(id string) + NotifyClient(id string, e Event) +} + +type client struct { + id string + eventChan chan Event +} + +func NewBasicNotifier() ClientNotifier { + return &basicNotifier{ + m: &sync.RWMutex{}, + clients: map[string]client{}, + } +} + +type basicNotifier struct { + m *sync.RWMutex + clients map[string]client +} + +func (p *basicNotifier) RegisterClient(id string) <-chan Event { + p.m.Lock() + defer p.m.Unlock() + + eventChan := make(chan Event) + p.clients[id] = client{ + id: id, + eventChan: eventChan, + } + + return eventChan +} + +func (p *basicNotifier) UnregisterClient(id string) { + p.m.Lock() + defer p.m.Unlock() + + delete(p.clients, id) +} + +func (p *basicNotifier) NotifyClient(id string, e Event) { + p.m.RLock() + defer p.m.RUnlock() + + client, ok := p.clients[id] + if !ok { + return + } + client.eventChan <- e +} diff --git a/config/config.go b/src/config/config.go similarity index 100% rename from config/config.go rename to src/config/config.go diff --git a/config/config_test.go b/src/config/config_test.go similarity index 100% rename from config/config_test.go rename to src/config/config_test.go diff --git a/config/new.go b/src/config/new.go similarity index 100% rename from config/new.go rename to src/config/new.go diff --git a/config/parser.go b/src/config/parser.go similarity index 100% rename from config/parser.go rename to src/config/parser.go diff --git a/src/models/action_token.go b/src/core/models/action_token.go similarity index 100% rename from src/models/action_token.go rename to src/core/models/action_token.go diff --git a/src/models/user.go b/src/core/models/user.go similarity index 100% rename from src/models/user.go rename to src/core/models/user.go diff --git a/src/repo/action_token.go b/src/core/repos/action_token.go similarity index 97% rename from src/repo/action_token.go rename to src/core/repos/action_token.go index 453a4ab..df2c26d 100644 --- a/src/repo/action_token.go +++ b/src/core/repos/action_token.go @@ -1,7 +1,7 @@ -package repo +package repos import ( - "backend/src/models" + "backend/src/core/models" "context" "database/sql" ) diff --git a/src/repo/cache_inmem.go b/src/core/repos/cache_inmem.go similarity index 98% rename from src/repo/cache_inmem.go rename to src/core/repos/cache_inmem.go index 7b27175..4b834ca 100644 --- a/src/repo/cache_inmem.go +++ b/src/core/repos/cache_inmem.go @@ -1,4 +1,4 @@ -package repo +package repos import ( "sync" diff --git a/src/repo/email_repo.go b/src/core/repos/email_repo.go similarity index 98% rename from src/repo/email_repo.go rename to src/core/repos/email_repo.go index 9dd6733..e1eb0b4 100644 --- a/src/repo/email_repo.go +++ b/src/core/repos/email_repo.go @@ -1,4 +1,4 @@ -package repo +package repos import ( "strings" diff --git a/src/repo/user_repo.go b/src/core/repos/user_repo.go similarity index 98% rename from src/repo/user_repo.go rename to src/core/repos/user_repo.go index c2651bd..d64db63 100644 --- a/src/repo/user_repo.go +++ b/src/core/repos/user_repo.go @@ -1,7 +1,7 @@ -package repo +package repos import ( - "backend/src/models" + "backend/src/core/models" "context" "database/sql" "errors" diff --git a/src/services/shortlink_service.go b/src/core/services/shortlink_service.go similarity index 55% rename from src/services/shortlink_service.go rename to src/core/services/shortlink_service.go index d3f2469..198690a 100644 --- a/src/services/shortlink_service.go +++ b/src/core/services/shortlink_service.go @@ -1,11 +1,9 @@ package services import ( - "backend/src/repo" + "backend/src/core/repos" + "backend/src/core/utils" "fmt" - "math/rand" - "strings" - "time" ) type ShortlinkService interface { @@ -15,38 +13,23 @@ type ShortlinkService interface { type NewShortlinkServiceParams struct { Endpoint string - Cache repo.Cache[string, string] + Cache repos.Cache[string, string] } func NewShortlinkSevice(params NewShortlinkServiceParams) ShortlinkService { return &shortlinkService{ - cache: params.Cache, + randomUtil: *utils.NewRand(), + cache: params.Cache, } } type shortlinkService struct { - cache repo.Cache[string, string] -} - -func (s *shortlinkService) randomStr() string { - src := rand.NewSource(time.Now().UnixMicro()) - randGen := rand.New(src) - - builder := strings.Builder{} - for i := 0; i < 9; i++ { - offset := 0x41 - if randGen.Int()%2 == 1 { - offset = 0x61 - } - - byte := offset + (randGen.Int() % 26) - builder.WriteRune(rune(byte)) - } - return builder.String() + randomUtil utils.RandomUtil + cache repos.Cache[string, string] } func (s *shortlinkService) CreateLink(in string) (string, error) { - str := s.randomStr() + str := s.randomUtil.RandomID(10, utils.CharsetAll) s.cache.Set(str, in, 7*24*60*60) return str, nil } diff --git a/src/services/user_service.go b/src/core/services/user_service.go similarity index 95% rename from src/services/user_service.go rename to src/core/services/user_service.go index f8e8433..3faf7fe 100644 --- a/src/services/user_service.go +++ b/src/core/services/user_service.go @@ -1,9 +1,9 @@ package services import ( - "backend/src/models" - "backend/src/repo" - "backend/src/utils" + "backend/src/core/models" + "backend/src/core/repos" + "backend/src/core/utils" "context" "fmt" @@ -32,10 +32,10 @@ func NewUserService(deps UserServiceDeps) UserService { type UserServiceDeps struct { Jwt utils.JwtUtil Password utils.PasswordUtil - UserRepo repo.UserRepo - UserCache repo.Cache[string, models.UserDTO] - EmailRepo repo.EmailRepo - ActionTokenRepo repo.ActionTokenRepo + UserRepo repos.UserRepo + UserCache repos.Cache[string, models.UserDTO] + EmailRepo repos.EmailRepo + ActionTokenRepo repos.ActionTokenRepo } type userService struct { diff --git a/src/utils/jwt.go b/src/core/utils/jwt.go similarity index 100% rename from src/utils/jwt.go rename to src/core/utils/jwt.go diff --git a/src/utils/password.go b/src/core/utils/password.go similarity index 100% rename from src/utils/password.go rename to src/core/utils/password.go diff --git a/src/core/utils/random.go b/src/core/utils/random.go new file mode 100644 index 0000000..df07126 --- /dev/null +++ b/src/core/utils/random.go @@ -0,0 +1,70 @@ +package utils + +import ( + "math/rand" + "strings" + "time" +) + +type Charset int + +const ( + CharsetAll Charset = iota + CharsetLettersLower + CharsetLettersUpper + CharsetLetters + CharsetNumeric +) + +type charsetPart struct { + Offset int + Size int +} + +var charsets = map[Charset][]charsetPart{} + +func NewRand() *RandomUtil { + charsetLettersLower := charsetPart{ //CharsetLettersLower + Offset: 0x41, + Size: 26, + } + + charsetLettersUpper := charsetPart{ //CharsetLettersUpper + Offset: 0x61, + Size: 26, + } + + charsetNumeric := charsetPart{ //CharsetLettersNumeric + Offset: 0x30, + Size: 10, + } + + charsets = map[Charset][]charsetPart{ + CharsetNumeric: {charsetNumeric}, + CharsetLettersLower: {charsetLettersLower}, + CharsetLettersUpper: {charsetLettersUpper}, + CharsetLetters: {charsetLettersLower, charsetLettersUpper}, + CharsetAll: {charsetLettersLower, charsetLettersUpper, charsetNumeric}, + } + + return &RandomUtil{} +} + +type RandomUtil struct{} + +func (r *RandomUtil) RandomID(outputLenght int, charset Charset) string { + src := rand.NewSource(time.Now().UnixMicro()) + randGen := rand.New(src) + + charsetData := charsets[charset] + + builder := strings.Builder{} + for i := 0; i < outputLenght; i++ { + charsetIdx := randGen.Int() % len(charsetData) + charsetPart := charsetData[charsetIdx] + + byte := charsetPart.Offset + (randGen.Int() % charsetPart.Size) + builder.WriteRune(rune(byte)) + } + return builder.String() +} diff --git a/src/leader_elector/elector.go b/src/leader_elector/elector.go new file mode 100644 index 0000000..4c9ac94 --- /dev/null +++ b/src/leader_elector/elector.go @@ -0,0 +1,25 @@ +package leader_elector + +import ( + "context" + "database/sql" +) + +func Lock(ctx context.Context, db *sql.DB, lockName, id string) error { + query := ` + update locks (id) + set id = $1 + where name == lockName and timestamp < $1 returning id + on conflict + insert into locks(id, name) values($1, $2);` + + row := db.QueryRowContext(ctx, query, id) + + result := "" + err := row.Scan(&result) + if err != nil { + return err + } + + return nil +} diff --git a/logger/event.go b/src/logger/event.go similarity index 100% rename from logger/event.go rename to src/logger/event.go diff --git a/logger/logger.go b/src/logger/logger.go similarity index 100% rename from logger/logger.go rename to src/logger/logger.go diff --git a/logger/new.go b/src/logger/new.go similarity index 100% rename from logger/new.go rename to src/logger/new.go diff --git a/src/handlers/dummy_handler.go b/src/server/handlers/dummy_handler.go similarity index 100% rename from src/handlers/dummy_handler.go rename to src/server/handlers/dummy_handler.go diff --git a/src/server/handlers/long_pooling_handler.go b/src/server/handlers/long_pooling_handler.go new file mode 100644 index 0000000..332436e --- /dev/null +++ b/src/server/handlers/long_pooling_handler.go @@ -0,0 +1,27 @@ +package handlers + +import ( + "backend/src/client_notifier" + "backend/src/server/utils" + + "github.com/gin-gonic/gin" +) + +func NewLongPoolingHandler(notifier client_notifier.ClientNotifier) gin.HandlerFunc { + return func(c *gin.Context) { + user := utils.GetUserFromRequest(c) + if user == nil { + c.Data(403, "plain/text", []byte("Unauthorized")) + return + } + + eventChan := notifier.RegisterClient(user.Id) + + select { + case <-c.Done(): + notifier.UnregisterClient(user.Id) + case event := <-eventChan: + c.Data(200, "application/json", event.Data) + } + } +} diff --git a/src/handlers/shortlink_handlers.go b/src/server/handlers/shortlink_handlers.go similarity index 97% rename from src/handlers/shortlink_handlers.go rename to src/server/handlers/shortlink_handlers.go index f228e8e..44e200e 100644 --- a/src/handlers/shortlink_handlers.go +++ b/src/server/handlers/shortlink_handlers.go @@ -1,7 +1,7 @@ package handlers import ( - "backend/src/services" + "backend/src/core/services" "encoding/json" "fmt" "net/url" diff --git a/src/handlers/user_create_handler.go b/src/server/handlers/user_create_handler.go similarity index 97% rename from src/handlers/user_create_handler.go rename to src/server/handlers/user_create_handler.go index b50f336..28ad72c 100644 --- a/src/handlers/user_create_handler.go +++ b/src/server/handlers/user_create_handler.go @@ -1,7 +1,7 @@ package handlers import ( - "backend/src/services" + "backend/src/core/services" "encoding/json" "github.com/gin-gonic/gin" diff --git a/src/handlers/user_login_handler.go b/src/server/handlers/user_login_handler.go similarity index 96% rename from src/handlers/user_login_handler.go rename to src/server/handlers/user_login_handler.go index 2a273e4..00eec5d 100644 --- a/src/handlers/user_login_handler.go +++ b/src/server/handlers/user_login_handler.go @@ -1,7 +1,7 @@ package handlers import ( - "backend/src/services" + "backend/src/core/services" "encoding/json" "github.com/gin-gonic/gin" diff --git a/src/middleware/auth.go b/src/server/middleware/auth.go similarity index 94% rename from src/middleware/auth.go rename to src/server/middleware/auth.go index 9392816..78419a0 100644 --- a/src/middleware/auth.go +++ b/src/server/middleware/auth.go @@ -1,7 +1,7 @@ package middleware import ( - "backend/src/services" + "backend/src/core/services" "fmt" "github.com/gin-gonic/gin" diff --git a/src/middleware/request_log.go b/src/server/middleware/request_log.go similarity index 97% rename from src/middleware/request_log.go rename to src/server/middleware/request_log.go index 5f79b58..889d75f 100644 --- a/src/middleware/request_log.go +++ b/src/server/middleware/request_log.go @@ -1,7 +1,7 @@ package middleware import ( - "backend/logger" + "backend/src/logger" "time" "github.com/gin-gonic/gin" diff --git a/src/server/utils/user.go b/src/server/utils/user.go new file mode 100644 index 0000000..0f6bb32 --- /dev/null +++ b/src/server/utils/user.go @@ -0,0 +1,14 @@ +package utils + +import ( + "backend/src/core/models" + + "github.com/gin-gonic/gin" +) + +func GetUserFromRequest(c *gin.Context) *models.UserDTO { + if user, ok := c.Get("user"); ok { + return user.(*models.UserDTO) + } + return nil +}