19 Commits
Author SHA1 Message Date
emil28092005 58cadadf7f Add JWT authorization functionality. 2026-01-21 04:20:44 +03:00
emil28092005 b36b19af27 Harbor works! 2026-01-18 22:27:16 +03:00
emil28092005 909d3e9162 Harbor works! 2026-01-18 22:26:18 +03:00
emil28092005 07b178b488 Harbor works! 2026-01-18 21:36:22 +03:00
emil28092005 82cc3d396a Set up Harbor. 2026-01-18 21:30:52 +03:00
emil28092005 acea25f536 Set up Harbor. 2026-01-18 21:10:26 +03:00
emil28092005 2883cb6045 Minimal CI/CD works! 2026-01-18 06:23:02 +03:00
emil28092005 aeed25da38 Testing Github webhook cicd. 2026-01-18 06:21:12 +03:00
emil28092005 a26fc340eb Testing Github webhook cicd. 2026-01-18 06:17:39 +03:00
emil28092005 2e9ad750de Fix nginx. 2026-01-18 06:13:23 +03:00
emil28092005 f93d66c35b Testing Github webhook cicd. 2026-01-18 06:02:11 +03:00
emil28092005 1acd6b67c4 Testing Github webhook cicd. 2026-01-18 05:58:35 +03:00
emil28092005 89c12dace6 Testing Github webhook cicd. 2026-01-18 05:53:20 +03:00
emil28092005 d115624d85 Testing Github webhook cicd. 2026-01-18 05:52:24 +03:00
emil28092005 5e6f526446 Testing Github webhook cicd. 2026-01-18 05:47:15 +03:00
emil28092005 d7427c55dd Fix Jenkinsfile. 2026-01-18 05:46:28 +03:00
emil28092005 e05d1000f8 Testing Github webhook cicd. 2026-01-18 05:40:31 +03:00
Emil Shanaty 8437a82428 Merge pull request #6 from emil28092005/cicd-fix
Tryiing to fix Jenkins.
2026-01-18 05:23:55 +03:00
Emil Shanaty 0e70e7d2bc Merge pull request #5 from emil28092005/cicd-fix
Cicd fix
2026-01-18 05:18:52 +03:00
7 changed files with 193 additions and 29 deletions
Vendored
+58 -26
View File
@@ -6,12 +6,13 @@ pipeline {
timestamps()
}
environment {
IMAGE_NAME = "shagram"
DEPLOY_DIR = "/opt/shagram/shagram/deploy/shagram"
environment {
REGISTRY = "158.160.92.116:8085"
REGISTRY_PROJECT = "shagram"
IMAGE_NAME = "shagram"
DEPLOY_DIR = "/opt/shagram/shagram/deploy/shagram"
COMPOSE_FILE = "${WORKSPACE}/deploy/shagram/compose.yaml"
}
}
stages {
stage('Checkout') {
@@ -25,43 +26,74 @@ pipeline {
}
}
stage('Build') {
stage('Detect branch') {
steps {
script {
env.GIT_BRANCH = sh(
script: "git name-rev --name-only --refs=refs/remotes/origin/* HEAD | sed 's#^remotes/##' | head -n1",
returnStdout: true
).trim()
echo "Detected branch: ${env.GIT_BRANCH}"
}
}
}
stage('Build image') {
steps {
script {
env.GIT_SHA = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
env.APP_IMAGE = "${IMAGE_NAME}:${env.GIT_SHA}"
// итоговый тег для Harbor
env.APP_IMAGE = "${REGISTRY}/${REGISTRY_PROJECT}/${IMAGE_NAME}:${env.GIT_SHA}"
}
sh '''
set -eux
docker build -t "$APP_IMAGE" .
# собираем локальный образ без registry
docker build -t "${IMAGE_NAME}:${GIT_SHA}" .
'''
}
}
stage('Login & Push to Harbor') {
steps {
withCredentials([usernamePassword(
credentialsId: 'harbor-creds',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASS'
)]) {
sh '''
set -eux
echo "$HARBOR_PASS" | docker login "$REGISTRY" -u "$HARBOR_USER" --password-stdin
docker tag "${IMAGE_NAME}:${GIT_SHA}" "${APP_IMAGE}"
docker push "${APP_IMAGE}"
'''
}
}
}
stage('Test') {
steps {
sh '''
echo "Testing..."
'''
sh 'echo "Testing..."'
}
}
stage('Deploy') {
when {
beforeAgent true
branch 'main'
}
steps {
sh '''
set -eux
mkdir -p "$DEPLOY_DIR"
cat > "$DEPLOY_DIR/.env" <<EOF
APP_IMAGE=$APP_IMAGE
EOF
docker compose -f "$COMPOSE_FILE" --project-directory "$DEPLOY_DIR" up -d --remove-orphans
'''
}
}
when {
expression { env.GIT_BRANCH == 'origin/main' }
}
steps {
sh '''
set -eux
mkdir -p "$DEPLOY_DIR"
cat > "$DEPLOY_DIR/.env" <<EOF
APP_IMAGE=${APP_IMAGE}
EOF
docker compose -f "$COMPOSE_FILE" --project-directory "$DEPLOY_DIR" pull
docker compose -f "$COMPOSE_FILE" --project-directory "$DEPLOY_DIR" up -d --remove-orphans
docker compose -f "$COMPOSE_FILE" --project-directory "$DEPLOY_DIR" ps
'''
}
}
}
}
+23
View File
@@ -5,9 +5,11 @@ import (
"net/http"
"os"
"shagram/internal/api"
"shagram/internal/auth"
"shagram/internal/db"
"shagram/internal/models"
"shagram/internal/websocket"
"time"
"github.com/gin-gonic/gin"
)
@@ -29,6 +31,27 @@ func main() {
router := gin.Default()
router.POST("/api/auth/login", func(c *gin.Context) {
var req struct {
Username string `json:"username"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "username required"})
return
}
token, err := auth.NewAccessToken(req.Username, time.Hour)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"access_token": token})
})
router.GET("/api/me", auth.Middleware(), func(c *gin.Context) {
username := c.GetString(auth.CtxUsernameKey)
c.JSON(200, gin.H{"username": username})
})
router.GET("/ws/:room", func(c *gin.Context) {
api.WebSocketHandler(hub, database)(c)
})
+4 -3
View File
@@ -1,16 +1,17 @@
services:
shagram:
image: ${APP_IMAGE:-shagram:local}
build:
build:
context: ../..
container_name: shagram-app
environment:
- DATABASE_PATH=/app/data/shagram.db
- JWT_SECRET=${JWT_SECRET}
volumes:
- shagram_data:/app/data
expose:
- "8080"
nginx:
image: nginx:alpine
container_name: shagram-nginx
@@ -23,4 +24,4 @@ services:
depends_on:
- shagram
volumes:
shagram_data:
shagram_data:
+1
View File
@@ -14,6 +14,7 @@ require (
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+2
View File
@@ -22,6 +22,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+64
View File
@@ -0,0 +1,64 @@
package auth
import (
"errors"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
func secret() ([]byte, error) {
s := os.Getenv("JWT_SECRET")
if s == "" {
return nil, errors.New("JWT_SECRET is not set")
}
return []byte(s), nil
}
func NewAccessToken(username string, ttl time.Duration) (string, error) {
key, err := secret()
if err != nil {
return "", err
}
now := time.Now()
claims := Claims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
Subject: username,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
},
}
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return t.SignedString(key)
}
func ParseAccessToken(tokenString string) (*Claims, error) {
key, err := secret()
if err != nil {
return nil, err
}
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
return key, nil
},
jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}),
)
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
+41
View File
@@ -0,0 +1,41 @@
package auth
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
const CtxUsernameKey = "username"
func Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
h := c.GetHeader("Authorization")
if h == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing Authorization header"})
return
}
const prefix = "Bearer "
if !strings.HasPrefix(h, prefix) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "expected Bearer token"})
return
}
raw := strings.TrimSpace(strings.TrimPrefix(h, prefix))
if raw == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "empty token"})
return
}
claims, err := ParseAccessToken(raw)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
c.Set(CtxUsernameKey, claims.Username)
c.Next()
}
}