Merge pull request #5 from Nucrea/feature/url-shortener
Feature/url shortener
This commit is contained in:
commit
73e7a25b11
9
.gitignore
vendored
9
.gitignore
vendored
@ -21,5 +21,14 @@
|
|||||||
go.work
|
go.work
|
||||||
go.work.sum
|
go.work.sum
|
||||||
|
|
||||||
|
# Binary dir
|
||||||
|
.build/
|
||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
.run
|
||||||
|
|
||||||
|
# temporary
|
||||||
|
coworker/
|
||||||
|
webapp/
|
||||||
@ -8,6 +8,7 @@ RUN go mod download && go mod verify
|
|||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go build -v -o /usr/local/bin/app ./...
|
RUN go build -v -o ./app .
|
||||||
|
RUN chmod +x ./app
|
||||||
|
|
||||||
CMD ["app"]
|
CMD ["./app", "-c", "./misc/config.yaml"]
|
||||||
34
main.go
34
main.go
@ -1,15 +1,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/args_parser"
|
"backend/src/args_parser"
|
||||||
"backend/config"
|
"backend/src/client_notifier"
|
||||||
"backend/logger"
|
"backend/src/config"
|
||||||
"backend/src/handlers"
|
"backend/src/core/models"
|
||||||
"backend/src/middleware"
|
"backend/src/core/repos"
|
||||||
"backend/src/models"
|
"backend/src/core/services"
|
||||||
"backend/src/repo"
|
"backend/src/core/utils"
|
||||||
"backend/src/services"
|
"backend/src/logger"
|
||||||
"backend/src/utils"
|
"backend/src/server/handlers"
|
||||||
|
"backend/src/server/middleware"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
@ -75,10 +76,12 @@ func main() {
|
|||||||
|
|
||||||
jwtUtil := utils.NewJwtUtil(key)
|
jwtUtil := utils.NewJwtUtil(key)
|
||||||
passwordUtil := utils.NewPasswordUtil()
|
passwordUtil := utils.NewPasswordUtil()
|
||||||
userRepo := repo.NewUserRepo(sqlDb)
|
userRepo := repos.NewUserRepo(sqlDb)
|
||||||
userCache := repo.NewCacheInmem[string, models.UserDTO](60 * 60)
|
userCache := repos.NewCacheInmem[string, models.UserDTO](60 * 60)
|
||||||
emailRepo := repo.NewEmailRepo()
|
emailRepo := repos.NewEmailRepo()
|
||||||
actionTokenRepo := repo.NewActionTokenRepo(sqlDb)
|
actionTokenRepo := repos.NewActionTokenRepo(sqlDb)
|
||||||
|
|
||||||
|
clientNotifier := client_notifier.NewBasicNotifier()
|
||||||
|
|
||||||
userService := services.NewUserService(
|
userService := services.NewUserService(
|
||||||
services.UserServiceDeps{
|
services.UserServiceDeps{
|
||||||
@ -92,7 +95,7 @@ func main() {
|
|||||||
)
|
)
|
||||||
linkService := services.NewShortlinkSevice(
|
linkService := services.NewShortlinkSevice(
|
||||||
services.NewShortlinkServiceParams{
|
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.Use(middleware.NewAuthMiddleware(userService))
|
||||||
dummyGroup.GET("/", handlers.NewDummyHandler())
|
dummyGroup.GET("/", handlers.NewDummyHandler())
|
||||||
|
|
||||||
|
lpGroup := r.Group("/pooling")
|
||||||
|
lpGroup.GET("/", handlers.NewLongPoolingHandler(clientNotifier))
|
||||||
|
|
||||||
listenAddr := fmt.Sprintf(":%d", conf.GetPort())
|
listenAddr := fmt.Sprintf(":%d", conf.GetPort())
|
||||||
logger.Log().Msgf("server listening on %s", listenAddr)
|
logger.Log().Msgf("server listening on %s", listenAddr)
|
||||||
|
|
||||||
|
|||||||
4
makefile
Normal file
4
makefile
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
all: release
|
||||||
|
|
||||||
|
release:
|
||||||
|
go build -o ./.build/release/backend main.go
|
||||||
12
src/client_notifier/event.go
Normal file
12
src/client_notifier/event.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package client_notifier
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Type EventType
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventTypeEmailConfirmed EventType = "event_email_confirmed"
|
||||||
|
)
|
||||||
57
src/client_notifier/notifier.go
Normal file
57
src/client_notifier/notifier.go
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package repo
|
package repos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/src/models"
|
"backend/src/core/models"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
)
|
)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package repo
|
package repos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package repo
|
package repos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package repo
|
package repos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/src/models"
|
"backend/src/core/models"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
@ -1,11 +1,9 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/src/repo"
|
"backend/src/core/repos"
|
||||||
|
"backend/src/core/utils"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShortlinkService interface {
|
type ShortlinkService interface {
|
||||||
@ -15,38 +13,23 @@ type ShortlinkService interface {
|
|||||||
|
|
||||||
type NewShortlinkServiceParams struct {
|
type NewShortlinkServiceParams struct {
|
||||||
Endpoint string
|
Endpoint string
|
||||||
Cache repo.Cache[string, string]
|
Cache repos.Cache[string, string]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewShortlinkSevice(params NewShortlinkServiceParams) ShortlinkService {
|
func NewShortlinkSevice(params NewShortlinkServiceParams) ShortlinkService {
|
||||||
return &shortlinkService{
|
return &shortlinkService{
|
||||||
|
randomUtil: *utils.NewRand(),
|
||||||
cache: params.Cache,
|
cache: params.Cache,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type shortlinkService struct {
|
type shortlinkService struct {
|
||||||
cache repo.Cache[string, string]
|
randomUtil utils.RandomUtil
|
||||||
}
|
cache repos.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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *shortlinkService) CreateLink(in string) (string, error) {
|
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)
|
s.cache.Set(str, in, 7*24*60*60)
|
||||||
return str, nil
|
return str, nil
|
||||||
}
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/src/models"
|
"backend/src/core/models"
|
||||||
"backend/src/repo"
|
"backend/src/core/repos"
|
||||||
"backend/src/utils"
|
"backend/src/core/utils"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
@ -32,10 +32,10 @@ func NewUserService(deps UserServiceDeps) UserService {
|
|||||||
type UserServiceDeps struct {
|
type UserServiceDeps struct {
|
||||||
Jwt utils.JwtUtil
|
Jwt utils.JwtUtil
|
||||||
Password utils.PasswordUtil
|
Password utils.PasswordUtil
|
||||||
UserRepo repo.UserRepo
|
UserRepo repos.UserRepo
|
||||||
UserCache repo.Cache[string, models.UserDTO]
|
UserCache repos.Cache[string, models.UserDTO]
|
||||||
EmailRepo repo.EmailRepo
|
EmailRepo repos.EmailRepo
|
||||||
ActionTokenRepo repo.ActionTokenRepo
|
ActionTokenRepo repos.ActionTokenRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
type userService struct {
|
type userService struct {
|
||||||
70
src/core/utils/random.go
Normal file
70
src/core/utils/random.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
25
src/leader_elector/elector.go
Normal file
25
src/leader_elector/elector.go
Normal file
@ -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
|
||||||
|
}
|
||||||
27
src/server/handlers/long_pooling_handler.go
Normal file
27
src/server/handlers/long_pooling_handler.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/src/services"
|
"backend/src/core/services"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/src/services"
|
"backend/src/core/services"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/src/services"
|
"backend/src/core/services"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/src/services"
|
"backend/src/core/services"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"backend/logger"
|
"backend/src/logger"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
14
src/server/utils/user.go
Normal file
14
src/server/utils/user.go
Normal file
@ -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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user