feat: increased security
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2026-02-21 21:05:47 +03:00
parent 04830681b9
commit 8befcc11c1
14 changed files with 233 additions and 50 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.git
.gitignore
.drone.yml
.env
README.md
docker-compose.yml

View File

@@ -11,13 +11,13 @@ steps:
- go test ./...
- name: build and push docker image
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- docker build -t localhost:5000/link-shortener:latest -f dockerfile .
- docker push localhost:5000/link-shortener:latest
image: plugins/docker
settings:
repo: localhost:5000/link-shortener
tags: latest
dockerfile: dockerfile
registry: localhost:5000
insecure: true
- name: deploy with compose
image: docker:24
@@ -35,7 +35,10 @@ steps:
LETSENCRYPT_EMAIL:
from_secret: LETSENCRYPT_EMAIL
SERVER_PORT: 8080
REDIS_URL: redis://redis:6379
REDIS_PASSWORD:
from_secret: REDIS_PASSWORD
REDIS_URL:
from_secret: REDIS_URL
commands:
- apk add --no-cache docker-cli-compose
- cp -r . /opt/app
@@ -49,8 +52,8 @@ volumes:
path: /var/run/docker.sock
- name: appdir
host:
path: /tmp/drone-app-deploy
path: /opt/drone/link-shortener
trigger:
branch:
- main
- main

1
.gitignore vendored
View File

@@ -0,0 +1 @@
.env

View File

@@ -35,7 +35,10 @@ func run() error {
address := fmt.Sprintf("0.0.0.0:%s", cfg.ServerPort)
repo := repositories.NewLinksRepository(database)
service := services.NewLinksService(repo)
service, err := services.NewLinksService(repo)
if err != nil {
log.Fatalf("failed to initialize links service: %v", err)
}
handler := handlers.NewLinksHandler(service, cfg.ServerHost, cfg.ServerPort)
server := httpserver.NewServer(address, handler)

View File

@@ -19,6 +19,14 @@ services:
- SERVER_PORT=${SERVER_PORT}
- REDIS_URL=${REDIS_URL}
restart: unless-stopped
read_only: true
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
memory: 256M
cpus: '0.50'
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/ping"]
interval: 5s
@@ -27,15 +35,25 @@ services:
start_period: 10s
redis:
image: redis:latest
image: redis:7.4-alpine
container_name: link-shortener-redis
networks: [isolated]
restart: unless-stopped
read_only: true
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
memory: 256M
cpus: '0.50'
volumes:
- redis_data:/data
command: ["redis-server", "--appendonly", "yes"]
tmpfs:
- /tmp
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 5s
timeout: 3s
retries: 5
@@ -49,4 +67,4 @@ networks:
isolated:
driver: bridge
proxy:
external: true
external: true

View File

@@ -7,12 +7,19 @@ RUN go mod download && go mod verify
COPY . ./
RUN go build -o bin/application ./cmd
RUN CGO_ENABLED=0 go build -o bin/application ./cmd
FROM alpine:3.21 AS runner
RUN addgroup -S app && adduser -S app -G app
WORKDIR /opt
COPY --from=builder --chown=app:app /opt/bin/application ./
USER app
CMD ["./application"]
COPY --from=builder /opt/bin/application ./
CMD ["./application"]

15
go.mod
View File

@@ -2,6 +2,14 @@ module example.com/m
go 1.25.3
require (
github.com/gin-gonic/gin v1.11.0
github.com/joho/godotenv v1.5.1
github.com/redis/go-redis/v9 v9.17.3
github.com/sqids/sqids-go v0.4.1
golang.org/x/time v0.14.0
)
require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
@@ -11,13 +19,11 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.11.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@@ -27,18 +33,13 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/redis/go-redis/v9 v9.17.3 // indirect
github.com/sqids/sqids-go v0.4.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

21
go.sum
View File

@@ -1,3 +1,7 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
@@ -9,6 +13,7 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
@@ -18,6 +23,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@@ -28,6 +35,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
@@ -46,6 +55,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
@@ -64,6 +74,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
@@ -74,21 +86,18 @@ golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -2,32 +2,27 @@ package db
import (
"context"
"log"
"fmt"
"time"
"example.com/m/internal/config"
"github.com/redis/go-redis/v9"
)
var Redis *redis.Client
func InitRedis(cfg *config.Config) (*redis.Client, error) {
opt, err := redis.ParseURL(cfg.RedisURL)
if err != nil {
log.Fatalf("impossible to parse redis url: %v", err)
return nil, err
return nil, fmt.Errorf("failed to parse redis url: %w", err)
}
Redis = redis.NewClient(opt)
client := redis.NewClient(opt)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := Redis.Ping(ctx).Err(); err != nil {
log.Fatalf("impossible to conenct to redis db: %v", err)
return nil, err
if err := client.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("failed to connect to redis: %w", err)
}
return Redis, nil
return client, nil
}

View File

@@ -3,6 +3,7 @@ package handlers
import (
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
@@ -47,7 +48,7 @@ func (h *LinksHandler) CreateLink(c *gin.Context) {
return
}
address := fmt.Sprintf("http://%v:%s/r/%v", h.host, h.port, id)
address := fmt.Sprintf("https://%s/r/%s", h.host, id)
response := CreateLinkResponse{
Status: "success",
@@ -89,5 +90,44 @@ func NormalizeURL(raw string) (string, error) {
return "", errors.New("invalid host in URL")
}
host := u.Hostname()
if isPrivateHost(host) {
return "", errors.New("URLs pointing to private/internal addresses are not allowed")
}
return u.String(), nil
}
func isPrivateHost(host string) bool {
ip := net.ParseIP(host)
if ip == nil {
addrs, err := net.LookupHost(host)
if err != nil || len(addrs) == 0 {
return false
}
ip = net.ParseIP(addrs[0])
if ip == nil {
return false
}
}
privateRanges := []string{
"127.0.0.0/8",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16",
"0.0.0.0/8",
"::1/128",
"fc00::/7",
"fe80::/10",
}
for _, cidr := range privateRanges {
_, network, _ := net.ParseCIDR(cidr)
if network.Contains(ip) {
return true
}
}
return false
}

View File

@@ -2,10 +2,10 @@ package http
import (
"fmt"
"log"
"net/http"
"example.com/m/internal/handlers"
"example.com/m/internal/middleware"
"github.com/gin-gonic/gin"
)
@@ -20,7 +20,12 @@ type Server struct {
func NewServer(address string, lh *handlers.LinksHandler) *Server {
engine := gin.New()
engine.Use(gin.Logger())
engine.Use(gin.Recovery())
engine.Use(middleware.SecurityHeaders())
rl := middleware.NewRateLimiter(10, 20)
engine.Use(rl.Middleware())
return &Server{
address: address,
@@ -32,11 +37,11 @@ func NewServer(address string, lh *handlers.LinksHandler) *Server {
func (s *Server) Start() error {
s.routes()
fmt.Printf("starting server on %s\n", s.address)
if err := s.engine.Run(s.address); err != nil && err != http.ErrServerClosed {
log.Fatalf("error occured while starting the server: %v", err)
return err
return fmt.Errorf("error occurred while starting the server: %w", err)
}
fmt.Println("server started: &v", s.address)
return nil
}

View File

@@ -0,0 +1,74 @@
package middleware
import (
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
type client struct {
limiter *rate.Limiter
lastSeen time.Time
}
type RateLimiter struct {
clients map[string]*client
mu sync.Mutex
rps rate.Limit
burst int
}
func NewRateLimiter(requestsPerSecond float64, burst int) *RateLimiter {
rl := &RateLimiter{
clients: make(map[string]*client),
rps: rate.Limit(requestsPerSecond),
burst: burst,
}
go rl.cleanup()
return rl
}
func (rl *RateLimiter) getClient(ip string) *rate.Limiter {
rl.mu.Lock()
defer rl.mu.Unlock()
if c, exists := rl.clients[ip]; exists {
c.lastSeen = time.Now()
return c.limiter
}
limiter := rate.NewLimiter(rl.rps, rl.burst)
rl.clients[ip] = &client{limiter: limiter, lastSeen: time.Now()}
return limiter
}
func (rl *RateLimiter) cleanup() {
for {
time.Sleep(time.Minute)
rl.mu.Lock()
for ip, c := range rl.clients {
if time.Since(c.lastSeen) > 3*time.Minute {
delete(rl.clients, ip)
}
}
rl.mu.Unlock()
}
}
func (rl *RateLimiter) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
limiter := rl.getClient(ip)
if !limiter.Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": "failed",
"message": "rate limit exceeded",
})
return
}
c.Next()
}
}

View File

@@ -0,0 +1,14 @@
package middleware
import "github.com/gin-gonic/gin"
func SecurityHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-Frame-Options", "DENY")
c.Header("X-XSS-Protection", "1; mode=block")
c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
c.Header("Content-Security-Policy", "default-src 'none'")
c.Next()
}
}

View File

@@ -1,6 +1,7 @@
package services
import (
"fmt"
"math/rand"
"example.com/m/internal/repositories"
@@ -12,17 +13,23 @@ type LinksService struct {
sqid *sqids.Sqids
}
func NewLinksService(repo *repositories.LinksRepository) *LinksService {
s, _ := sqids.New()
func NewLinksService(repo *repositories.LinksRepository) (*LinksService, error) {
s, err := sqids.New()
if err != nil {
return nil, fmt.Errorf("failed to initialize sqids: %w", err)
}
return &LinksService{
repo: repo,
sqid: s,
}
}, nil
}
func (s *LinksService) CreateLink(original string) (string, error) {
id, _ := s.sqid.Encode([]uint64{rand.Uint64()})
err := s.repo.CreateLink(id, original)
id, err := s.sqid.Encode([]uint64{rand.Uint64()})
if err != nil {
return "", fmt.Errorf("failed to encode link id: %w", err)
}
err = s.repo.CreateLink(id, original)
return id, err
}