init from gitlab

This commit is contained in:
texm
2023-04-25 14:33:14 +08:00
parent 6a85a41ff0
commit c8202a5c82
281 changed files with 19861 additions and 1 deletions

View File

@@ -0,0 +1,111 @@
package setup
import (
"context"
"database/sql"
"errors"
"fmt"
"gitlab.com/texm/shokku/internal/env"
"gitlab.com/texm/shokku/internal/models"
"gitlab.com/texm/shokku/internal/server/auth"
"gitlab.com/texm/shokku/internal/server/dto"
"gitlab.com/texm/shokku/internal/server/github"
"net/http"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
)
const (
githubAppInstallURL = "https://github.com/apps/%s/installations/new/permissions?target_id=%d"
)
func GetGithubSetupStatus(e *env.Env, c echo.Context) error {
var ghApp models.GithubApp
r := e.DB.Find(&ghApp)
if r.Error != nil && !errors.Is(r.Error, sql.ErrNoRows) {
log.Error().Err(r.Error).Msg("Failed to lookup github app")
return echo.ErrInternalServerError
}
return c.JSON(http.StatusOK, dto.GetGithubSetupStatus{
AppCreated: r.RowsAffected > 0,
})
}
func CreateGithubApp(e *env.Env, c echo.Context) error {
var req dto.CreateGithubAppRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
cfg, err := github.CompleteAppManifest(e, req.Code)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, dto.CreateGithubAppResponse{
Slug: cfg.GetSlug(),
})
}
func GetGithubAppInstallInfo(e *env.Env, c echo.Context) error {
ctx := context.Background()
client, clientErr := github.GetAppClient(e)
if clientErr != nil {
log.Error().Err(clientErr).Msg("failed to get app client")
return echo.NewHTTPError(http.StatusBadRequest, clientErr)
}
app, appErr := client.GetApp(ctx)
if appErr != nil {
log.Error().Err(appErr).Msg("failed to get github client app")
return echo.NewHTTPError(http.StatusBadRequest, appErr)
}
url := fmt.Sprintf(githubAppInstallURL, app.GetSlug(), app.Owner.GetID())
return c.JSON(http.StatusOK, dto.InstallGithubAppResponse{
InstallURL: url,
})
}
func CompleteGithubSetup(e *env.Env, c echo.Context) error {
var req dto.CompleteGithubSetupRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
client, clientErr := github.GetAppClient(e)
if clientErr != nil {
log.Error().Err(clientErr).Msg("failed to get app client")
return echo.ErrBadRequest
}
ctx := context.Background()
inst, _, instErr := client.Apps.GetInstallation(ctx, req.InstallationId)
if instErr != nil {
log.Error().
Err(instErr).
Int64("id", req.InstallationId).
Msg("failed to get installation")
return echo.ErrBadRequest
}
if err := setupServerWithAuthMethod(e, auth.MethodGithub); err != nil {
log.Error().Err(err).Msg("failed to setup github auth")
return echo.ErrInternalServerError
}
go func() {
if err := github.SyncUsersToDB(e); err != nil {
log.Error().Err(err).Msg("failed to sync github users")
}
}()
log.Debug().
Int64("id", inst.GetID()).
Msg("installed github app")
return c.NoContent(http.StatusOK)
}

View File

@@ -0,0 +1,101 @@
package setup
import (
"bytes"
"encoding/base64"
"fmt"
"github.com/rs/zerolog/log"
"gitlab.com/texm/shokku/internal/models"
"gitlab.com/texm/shokku/internal/server/auth"
"image/png"
"math/rand"
"net/http"
"github.com/labstack/echo/v4"
"github.com/pquerna/otp/totp"
"gitlab.com/texm/shokku/internal/env"
"gitlab.com/texm/shokku/internal/server/dto"
)
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func generateRandomString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
func GenerateTotp(e *env.Env, c echo.Context) error {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "shokku",
AccountName: "admin account",
})
if err != nil {
return fmt.Errorf("failed to generate totp key: %w", err)
}
var imgBuf bytes.Buffer
img, imgErr := key.Image(160, 160)
if imgErr != nil {
return fmt.Errorf("failed to generate totp image: %w", imgErr)
}
if encErr := png.Encode(&imgBuf, img); encErr != nil {
return fmt.Errorf("failed to encode totp image to png: %w", encErr)
}
return c.JSON(http.StatusOK, dto.GenerateTotpResponse{
Secret: key.Secret(),
Image: base64.StdEncoding.EncodeToString(imgBuf.Bytes()),
RecoveryCode: generateRandomString(12),
})
}
func ConfirmTotp(e *env.Env, c echo.Context) error {
var req dto.ConfirmTotpRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
return c.JSON(http.StatusOK, dto.ConfirmTotpResponse{
Valid: totp.Validate(req.Code, req.Secret),
})
}
func CompletePasswordSetup(e *env.Env, c echo.Context) error {
var req dto.CompletePasswordSetupRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
if err := setupServerWithAuthMethod(e, auth.MethodPassword); err != nil {
log.Error().Err(err).Msg("failed to setup github auth")
return echo.ErrInternalServerError
}
pw, ok := e.Auth.(*auth.PasswordAuthenticator)
if !ok {
log.Error().Msg("failed to cast e.Auth to pw auth")
return echo.ErrInternalServerError
}
passwordHash, pwErr := pw.HashPassword([]byte(req.Password))
if pwErr != nil {
log.Error().Err(pwErr).Msg("failed to hash password")
return echo.ErrInternalServerError
}
user := models.User{
Name: req.Username,
Source: "manual",
PasswordHash: passwordHash,
TotpEnabled: req.Enable2FA,
RecoveryCode: req.RecoveryCode,
TotpSecret: req.TotpSecret,
}
if err := e.DB.Save(&user).Error; err != nil {
log.Error().Err(err).Msg("failed to save initial password auth user")
return echo.ErrInternalServerError
}
return c.NoContent(http.StatusOK)
}

View File

@@ -0,0 +1,20 @@
package setup
import (
"github.com/labstack/echo/v4"
"gitlab.com/texm/shokku/internal/env"
)
func RegisterRoutes(e *env.Env, g *echo.Group) {
g.GET("/status", e.H(GetStatus))
g.GET("/verify-key", e.H(GetSetupKeyValid))
g.POST("/github/create-app", e.H(CreateGithubApp))
g.GET("/github/install-info", e.H(GetGithubAppInstallInfo))
g.GET("/github/status", e.H(GetGithubSetupStatus))
g.POST("/github/completed", e.H(CompleteGithubSetup))
g.POST("/password", e.H(CompletePasswordSetup))
g.POST("/totp/new", e.H(GenerateTotp))
g.POST("/totp/confirm", e.H(ConfirmTotp))
}

View File

@@ -0,0 +1,65 @@
package setup
import (
"fmt"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"gitlab.com/texm/shokku/internal/env"
"gitlab.com/texm/shokku/internal/models"
"gitlab.com/texm/shokku/internal/server/auth"
"gitlab.com/texm/shokku/internal/server/dto"
"net/http"
)
func GetStatus(e *env.Env, c echo.Context) error {
method := string(e.Auth.GetMethod())
log.Debug().
Bool("is_setup", e.SetupCompleted).
Str("method", method).
Msg("get setup status")
return c.JSON(http.StatusOK, dto.GetSetupStatusResponse{
IsSetup: e.SetupCompleted,
Method: method,
})
}
func GetSetupKeyValid(e *env.Env, c echo.Context) error {
return c.NoContent(http.StatusOK)
}
func setupServerWithAuthMethod(e *env.Env, method auth.Method) error {
var state models.Server
e.DB.FirstOrCreate(&state)
state.IsSetup = true
state.AuthMethod = method
if err := e.DB.Save(&state).Error; err != nil {
log.Error().Err(err).Msg("failed to save setup state")
return echo.ErrInternalServerError
}
newAuth, authErr := createAuthenticator(e, method)
if authErr != nil {
log.Error().Err(authErr).Msg("failed to init new authenticator")
return echo.ErrInternalServerError
}
e.Auth = newAuth
e.SetupCompleted = true
return nil
}
func createAuthenticator(e *env.Env, method auth.Method) (auth.Authenticator, error) {
config := auth.Config{
SigningKey: e.Auth.GetSigningKey(),
CookieDomain: e.Auth.GetCookieDomain(),
TokenLifetime: e.Auth.GetTokenLifetime(),
}
switch method {
case auth.MethodGithub:
return auth.NewGithubAuthenticator(config)
case auth.MethodPassword:
return auth.NewPasswordAuthenticator(config, auth.DefaultBCryptCost)
}
return nil, fmt.Errorf("unknown method %s", method)
}