init from gitlab
This commit is contained in:
111
internal/server/api/setup/github.go
Normal file
111
internal/server/api/setup/github.go
Normal 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)
|
||||
}
|
||||
101
internal/server/api/setup/password.go
Normal file
101
internal/server/api/setup/password.go
Normal 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)
|
||||
}
|
||||
20
internal/server/api/setup/routes.go
Normal file
20
internal/server/api/setup/routes.go
Normal 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))
|
||||
}
|
||||
65
internal/server/api/setup/setup.go
Normal file
65
internal/server/api/setup/setup.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user