| package main
|
|
|
| import (
|
| "context"
|
| "encoding/json"
|
| "fmt"
|
| "html/template"
|
| "net/http"
|
| "strconv"
|
| "strings"
|
| "time"
|
|
|
| "atlassian/auth"
|
| "atlassian/db"
|
|
|
| "github.com/gin-gonic/gin"
|
| "github.com/go-resty/resty/v2"
|
| )
|
|
|
|
|
| func SetupRoutes() *gin.Engine {
|
| gin.SetMode(gin.ReleaseMode)
|
|
|
| r := gin.Default()
|
|
|
|
|
| r.Use(func(c *gin.Context) {
|
| c.Header("Access-Control-Allow-Origin", "*")
|
| c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
| c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
|
| if c.Request.Method == "OPTIONS" {
|
| c.AbortWithStatus(http.StatusOK)
|
| return
|
| }
|
|
|
| c.Next()
|
| })
|
|
|
|
|
| r.GET("/health", func(c *gin.Context) {
|
| c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
| })
|
|
|
|
|
| v1 := r.Group("/v1")
|
| {
|
| v1.GET("/models", ListModels)
|
| v1.POST("/chat/completions", ChatCompletions)
|
| }
|
|
|
|
|
| admin := r.Group("/admin")
|
| {
|
|
|
| admin.GET("/login", ShowLoginPage)
|
| admin.POST("/login", HandleLogin)
|
|
|
|
|
| authorized := admin.Group("/")
|
| authorized.Use(AuthMiddleware())
|
| {
|
|
|
| authorized.GET("/credentials", ShowCredentialsPage)
|
| authorized.POST("/credentials", AddCredential)
|
| authorized.POST("/credentials/delete/:id", DeleteCredential)
|
| authorized.GET("/credentials/reload", ReloadCredentialsHandler)
|
|
|
|
|
| authorized.POST("/apitoken/generate", GenerateAPITokenHandler)
|
|
|
|
|
| authorized.GET("/change-password", ShowChangePasswordPage)
|
| authorized.POST("/change-password", ChangePassword)
|
| authorized.GET("/reset-password", ShowResetPasswordPage)
|
| authorized.POST("/reset-password", ResetPassword)
|
| }
|
| }
|
|
|
|
|
| templ := template.Must(template.New("").ParseFS(GetTemplatesFS(), "templates/*.html"))
|
| r.SetHTMLTemplate(templ)
|
|
|
|
|
| r.StaticFS("/static", GetStaticFS())
|
|
|
| return r
|
| }
|
|
|
|
|
| func AuthMiddleware() gin.HandlerFunc {
|
| return func(c *gin.Context) {
|
|
|
| tokenString, err := c.Cookie("admin_jwt")
|
| if err != nil {
|
|
|
| c.Redirect(http.StatusFound, "/admin/login")
|
| c.Abort()
|
| return
|
| }
|
|
|
|
|
| claims, err := auth.ParseToken(tokenString)
|
| if err != nil {
|
|
|
| c.SetCookie("admin_jwt", "", -1, "/", "", false, true)
|
| c.Redirect(http.StatusFound, "/admin/login")
|
| c.Abort()
|
| return
|
| }
|
|
|
|
|
| isInitial, err := db.IsPasswordInitial()
|
| if err == nil && isInitial {
|
|
|
| if c.Request.URL.Path != "/admin/change-password" {
|
| c.Redirect(http.StatusFound, "/admin/change-password")
|
| c.Abort()
|
| return
|
| }
|
| }
|
|
|
|
|
| c.Set("userID", claims.UserID)
|
| c.Next()
|
| }
|
| }
|
|
|
|
|
| func ShowLoginPage(c *gin.Context) {
|
| c.HTML(http.StatusOK, "login.html", gin.H{
|
| "title": "Admin Login",
|
| })
|
| }
|
|
|
|
|
| func HandleLogin(c *gin.Context) {
|
| password := c.PostForm("password")
|
|
|
|
|
| storedHash, isInitial, err := db.GetAdminPassword()
|
| fmt.Println(isInitial)
|
| if err != nil {
|
| c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
| "error": "Failed to get password: " + err.Error(),
|
| })
|
| return
|
| }
|
|
|
|
|
| if auth.VerifyPassword(storedHash, password) {
|
|
|
| token, err := auth.GenerateToken(1)
|
| if err != nil {
|
| c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
| "error": "Failed to generate token: " + err.Error(),
|
| })
|
| return
|
| }
|
|
|
|
|
| c.SetCookie("admin_jwt", token, 3600, "/", "", false, true)
|
|
|
|
|
| if isInitial {
|
| c.Redirect(http.StatusFound, "/admin/change-password")
|
| } else {
|
| c.Redirect(http.StatusFound, "/admin/credentials")
|
| }
|
| } else {
|
| c.HTML(http.StatusOK, "login.html", gin.H{
|
| "title": "Admin Login",
|
| "error": "Incorrect password",
|
| })
|
| }
|
| }
|
|
|
|
|
| func ShowCredentialsPage(c *gin.Context) {
|
|
|
| credentials, err := db.GetAllCredentials()
|
| if err != nil {
|
| c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
| "error": "Failed to get credentials: " + err.Error(),
|
| })
|
| return
|
| }
|
|
|
|
|
| apiToken, _ := db.GetAPIToken()
|
|
|
| c.HTML(http.StatusOK, "credentials.html", gin.H{
|
| "title": "Credential Management",
|
| "credentials": credentials,
|
| "apiToken": apiToken,
|
| })
|
| }
|
|
|
|
|
| func AddCredential(c *gin.Context) {
|
| email := c.PostForm("email")
|
| token := c.PostForm("token")
|
|
|
|
|
| if email == "" || token == "" {
|
| c.HTML(http.StatusBadRequest, "error.html", gin.H{
|
| "error": "Email and token cannot be empty",
|
| })
|
| return
|
| }
|
|
|
|
|
| err := db.AddCredential(email, token)
|
| if err != nil {
|
| c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
| "error": "Failed to add credential: " + err.Error(),
|
| })
|
| return
|
| }
|
|
|
|
|
| ReloadCredentials()
|
|
|
|
|
| c.Redirect(http.StatusFound, "/admin/credentials")
|
| }
|
|
|
|
|
| func DeleteCredential(c *gin.Context) {
|
| idStr := c.Param("id")
|
| id, err := strconv.ParseUint(idStr, 10, 32)
|
| if err != nil {
|
| c.HTML(http.StatusBadRequest, "error.html", gin.H{
|
| "error": "Invalid ID",
|
| })
|
| return
|
| }
|
|
|
|
|
| err = db.DeleteCredential(uint(id))
|
| if err != nil {
|
| c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
| "error": "Failed to delete credential: " + err.Error(),
|
| })
|
| return
|
| }
|
|
|
|
|
| ReloadCredentials()
|
|
|
|
|
| c.Redirect(http.StatusFound, "/admin/credentials")
|
| }
|
|
|
|
|
| func ReloadCredentialsHandler(c *gin.Context) {
|
| ReloadCredentials()
|
| c.Redirect(http.StatusFound, "/admin/credentials")
|
| }
|
|
|
|
|
| func GenerateAPITokenHandler(c *gin.Context) {
|
| _, err := db.GenerateAPIToken()
|
| if err != nil {
|
| c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
| "error": "Failed to generate API token: " + err.Error(),
|
| })
|
| return
|
| }
|
|
|
| c.Redirect(http.StatusFound, "/admin/credentials")
|
| }
|
|
|
|
|
| func ShowChangePasswordPage(c *gin.Context) {
|
|
|
| isInitial, _ := db.IsPasswordInitial()
|
|
|
| c.HTML(http.StatusOK, "change_password.html", gin.H{
|
| "title": "Change Password",
|
| "isInitial": isInitial,
|
| })
|
| }
|
|
|
|
|
| func ChangePassword(c *gin.Context) {
|
|
|
| currentPassword := c.PostForm("current_password")
|
| newPassword := c.PostForm("new_password")
|
| confirmPassword := c.PostForm("confirm_password")
|
|
|
|
|
| if newPassword == "" {
|
| c.HTML(http.StatusBadRequest, "change_password.html", gin.H{
|
| "title": "Change Password",
|
| "error": "New password cannot be empty",
|
| })
|
| return
|
| }
|
|
|
| if newPassword != confirmPassword {
|
| c.HTML(http.StatusBadRequest, "change_password.html", gin.H{
|
| "title": "Change Password",
|
| "error": "Passwords do not match",
|
| })
|
| return
|
| }
|
|
|
|
|
| storedHash, _, err := db.GetAdminPassword()
|
| if err != nil {
|
| c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
| "error": "Failed to get password: " + err.Error(),
|
| })
|
| return
|
| }
|
|
|
|
|
| if !auth.VerifyPassword(storedHash, currentPassword) {
|
| c.HTML(http.StatusBadRequest, "change_password.html", gin.H{
|
| "title": "Change Password",
|
| "error": "Current password is incorrect",
|
| })
|
| return
|
| }
|
|
|
|
|
| newHash := auth.HashPassword(newPassword)
|
| err = db.SetAdminPassword(newHash, false)
|
| if err != nil {
|
| c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
| "error": "Failed to update password: " + err.Error(),
|
| })
|
| return
|
| }
|
|
|
|
|
| c.SetCookie("admin_jwt", "", -1, "/", "", false, true)
|
|
|
|
|
| c.Redirect(http.StatusFound, "/admin/login?message=Password updated, please login again")
|
| }
|
|
|
|
|
| func ShowResetPasswordPage(c *gin.Context) {
|
| c.HTML(http.StatusOK, "reset_password.html", gin.H{
|
| "title": "Reset Password",
|
| })
|
| }
|
|
|
|
|
| func ResetPassword(c *gin.Context) {
|
|
|
| newPassword := db.GenerateRandomPassword(12)
|
| newHash := auth.HashPassword(newPassword)
|
|
|
|
|
| err := db.SetAdminPassword(newHash, true)
|
| if err != nil {
|
| c.HTML(http.StatusInternalServerError, "error.html", gin.H{
|
| "error": "Failed to reset password: " + err.Error(),
|
| })
|
| return
|
| }
|
|
|
|
|
| c.SetCookie("admin_jwt", "", -1, "/", "", false, true)
|
|
|
|
|
| c.HTML(http.StatusOK, "password_reset_success.html", gin.H{
|
| "title": "Password Reset",
|
| "password": newPassword,
|
| })
|
| }
|
|
|
|
|
| func ListModels(c *gin.Context) {
|
| now := time.Now().Unix()
|
|
|
| models := make([]Model, len(SupportedModels))
|
| for i, modelID := range SupportedModels {
|
| models[i] = Model{
|
| ID: modelID,
|
| Object: "model",
|
| Created: now,
|
| OwnedBy: "system",
|
| }
|
| }
|
|
|
| response := ModelsResponse{
|
| Object: "list",
|
| Data: models,
|
| }
|
|
|
| c.JSON(http.StatusOK, response)
|
| }
|
|
|
|
|
| func ChatCompletions(c *gin.Context) {
|
|
|
| authHeader := c.GetHeader("Authorization")
|
| if authHeader == "" {
|
| c.JSON(http.StatusUnauthorized, gin.H{"error": "API key is required"})
|
| return
|
| }
|
|
|
|
|
| tokenParts := strings.Split(authHeader, " ")
|
| if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
|
| c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key format"})
|
| return
|
| }
|
|
|
| apiToken := tokenParts[1]
|
| if !db.ValidateAPIToken(apiToken) {
|
| c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"})
|
| return
|
| }
|
|
|
| var req ChatCompletionRequest
|
| if err := c.ShouldBindJSON(&req); err != nil {
|
| c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
|
| return
|
| }
|
|
|
|
|
| if req.Model == "" {
|
| c.JSON(http.StatusBadRequest, gin.H{"error": "Model is required"})
|
| return
|
| }
|
|
|
| if len(req.Messages) == 0 {
|
| c.JSON(http.StatusBadRequest, gin.H{"error": "Messages are required"})
|
| return
|
| }
|
|
|
| request := req.ToOpenAIRequest()
|
|
|
|
|
| atlassianReq := AtlassianRequest{
|
| RequestPayload: AtlassianRequestPayload{
|
| Messages: request.Messages,
|
| Temperature: req.Temperature,
|
| Stream: req.Stream,
|
| },
|
| PlatformAttributes: AtlassianPlatformAttrs{
|
| Model: TransformModelID(req.Model),
|
| },
|
| }
|
|
|
|
|
| client := NewHTTPClient()
|
| ctx := c.Request.Context()
|
|
|
|
|
| resp, err := client.FetchWithRetry(ctx, atlassianReq, req.Stream)
|
| if err != nil {
|
| c.JSON(http.StatusBadGateway, gin.H{"error": "All credentials exhausted"})
|
| return
|
| }
|
|
|
|
|
| if req.Stream {
|
| handleStreamingResponse(c, resp, req.Model)
|
| return
|
| }
|
|
|
|
|
| handleNonStreamingResponse(c, resp, req.Model)
|
| }
|
|
|
|
|
| func handleStreamingResponse(c *gin.Context, resp *resty.Response, requestedModel string) {
|
|
|
| c.Header("Content-Type", "text/event-stream")
|
| c.Header("Cache-Control", "no-cache")
|
| c.Header("Connection", "keep-alive")
|
|
|
|
|
| streamResp := &StreamResponse{
|
| Response: resp,
|
| Model: requestedModel,
|
| }
|
|
|
| ctx := c.Request.Context()
|
| dataChan, errChan := streamResp.ConvertToOpenAIStream(ctx)
|
|
|
|
|
| c.Writer.Header().Set("Content-Type", "text/event-stream")
|
| c.Writer.Header().Set("Cache-Control", "no-cache")
|
| c.Writer.Header().Set("Connection", "keep-alive")
|
|
|
| flusher, ok := c.Writer.(http.Flusher)
|
| if !ok {
|
| c.JSON(http.StatusInternalServerError, gin.H{"error": "Streaming not supported"})
|
| return
|
| }
|
|
|
| for {
|
| select {
|
| case data, ok := <-dataChan:
|
| if !ok {
|
| return
|
| }
|
| c.Writer.Write(data)
|
| flusher.Flush()
|
| case err := <-errChan:
|
| if err != nil && err != context.Canceled {
|
| c.Writer.Write([]byte("data: {\"error\":\"" + err.Error() + "\"}\n\n"))
|
| flusher.Flush()
|
| }
|
| return
|
| case <-ctx.Done():
|
| return
|
| }
|
| }
|
| }
|
|
|
|
|
| func handleNonStreamingResponse(c *gin.Context, resp *resty.Response, requestedModel string) {
|
| var atlassianResp AtlassianResponse
|
| if err := json.Unmarshal(resp.Body(), &atlassianResp); err != nil {
|
| c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse upstream response"})
|
| return
|
| }
|
|
|
|
|
| openaiResp := ToOpenAI(atlassianResp, requestedModel)
|
| c.JSON(http.StatusOK, openaiResp)
|
| }
|
|
|