mirror of
https://github.com/thomiceli/opengist.git
synced 2025-06-18 08:07:12 +02:00
Support Github and Gitea OAuth providers
This commit is contained in:
@ -1,8 +1,6 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/mattn/go-sqlite3"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
@ -38,7 +36,7 @@ func setSetting(key string, value string) error {
|
||||
func initAdminSettings(settings map[string]string) error {
|
||||
for key, value := range settings {
|
||||
if err := setSetting(key, value); err != nil {
|
||||
if !isUniqueConstraintViolation(err) {
|
||||
if !IsUniqueConstraintViolation(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -46,11 +44,3 @@ func initAdminSettings(settings map[string]string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isUniqueConstraintViolation(err error) bool {
|
||||
var sqliteErr sqlite3.Error
|
||||
if errors.As(err, &sqliteErr) && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/mattn/go-sqlite3"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
@ -32,3 +34,11 @@ func CountAll(table interface{}) (int64, error) {
|
||||
err := db.Model(table).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func IsUniqueConstraintViolation(err error) bool {
|
||||
var sqliteErr sqlite3.Error
|
||||
if errors.As(err, &sqliteErr) && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -1,6 +1,12 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SSHKey struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
@ -13,6 +19,16 @@ type SSHKey struct {
|
||||
User User `validate:"-" `
|
||||
}
|
||||
|
||||
func (sshKey *SSHKey) BeforeCreate(tx *gorm.DB) error {
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sha := sha256.Sum256(pubKey.Marshal())
|
||||
sshKey.SHA = base64.StdEncoding.EncodeToString(sha[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSSHKeysByUserID(userId uint) ([]*SSHKey, error) {
|
||||
var sshKeys []*SSHKey
|
||||
err := db.
|
||||
|
@ -12,6 +12,8 @@ type User struct {
|
||||
CreatedAt int64
|
||||
Email string
|
||||
MD5Hash string // for gravatar, if no Email is specified, the value is random
|
||||
GithubID string
|
||||
GiteaID string
|
||||
|
||||
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
|
||||
SSHKeys []SSHKey `gorm:"foreignKey:UserID"`
|
||||
@ -90,6 +92,19 @@ func GetUserBySSHKeyID(sshKeyId uint) (*User, error) {
|
||||
return user, err
|
||||
}
|
||||
|
||||
func GetUserByProvider(id string, provider string) (*User, error) {
|
||||
user := new(User)
|
||||
var err error
|
||||
switch provider {
|
||||
case "github":
|
||||
err = db.Where("github_id = ?", id).First(&user).Error
|
||||
case "gitea":
|
||||
err = db.Where("gitea_id = ?", id).First(&user).Error
|
||||
}
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (user *User) Create() error {
|
||||
return db.Create(&user).Error
|
||||
}
|
||||
@ -118,6 +133,17 @@ func (user *User) HasLiked(gist *Gist) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (user *User) DeleteProvider(provider string) error {
|
||||
switch provider {
|
||||
case "github":
|
||||
return db.Model(&user).Update("github_id", nil).Error
|
||||
case "gitea":
|
||||
return db.Model(&user).Update("gitea_id", nil).Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// -- DTO -- //
|
||||
|
||||
type UserDTO struct {
|
||||
|
@ -1,11 +1,18 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"opengist/internal/models"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func register(ctx echo.Context) error {
|
||||
@ -106,6 +113,143 @@ func processLogin(ctx echo.Context) error {
|
||||
return redirect(ctx, "/")
|
||||
}
|
||||
|
||||
func oauthCallback(ctx echo.Context) error {
|
||||
user, err := gothic.CompleteUserAuth(ctx.Response(), ctx.Request())
|
||||
if err != nil {
|
||||
return errorRes(400, "Cannot complete user auth", err)
|
||||
}
|
||||
|
||||
currUser := getUserLogged(ctx)
|
||||
if currUser != nil {
|
||||
// if user is logged in, link account to user
|
||||
switch user.Provider {
|
||||
case "github":
|
||||
currUser.GithubID = user.UserID
|
||||
case "gitea":
|
||||
currUser.GiteaID = user.UserID
|
||||
}
|
||||
|
||||
if err = currUser.Update(); err != nil {
|
||||
return errorRes(500, "Cannot update user "+strings.Title(user.Provider)+" id", err)
|
||||
}
|
||||
|
||||
addFlash(ctx, "Account linked to "+strings.Title(user.Provider), "success")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
// if user is not in database, create it
|
||||
userDB, err := models.GetUserByProvider(user.UserID, user.Provider)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorRes(500, "Cannot get user", err)
|
||||
}
|
||||
|
||||
userDB = &models.User{
|
||||
Username: user.NickName,
|
||||
Email: user.Email,
|
||||
MD5Hash: fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(user.Email))))),
|
||||
}
|
||||
|
||||
switch user.Provider {
|
||||
case "github":
|
||||
userDB.GithubID = user.UserID
|
||||
case "gitea":
|
||||
userDB.GiteaID = user.UserID
|
||||
}
|
||||
|
||||
if err = userDB.Create(); err != nil {
|
||||
if models.IsUniqueConstraintViolation(err) {
|
||||
addFlash(ctx, "Username "+user.NickName+" already exists in opengist", "error")
|
||||
return redirect(ctx, "/login")
|
||||
}
|
||||
|
||||
return errorRes(500, "Cannot create user", err)
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
switch user.Provider {
|
||||
case "github":
|
||||
resp, err = http.Get("https://github.com/" + user.NickName + ".keys")
|
||||
case "gitea":
|
||||
resp, err = http.Get("https://gitea.com/" + user.NickName + ".keys")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
addFlash(ctx, "Could not get user keys", "error")
|
||||
log.Error().Err(err).Msg("Could not get user keys")
|
||||
}
|
||||
|
||||
keys := strings.Split(string(body), "\n")
|
||||
if len(keys[len(keys)-1]) == 0 {
|
||||
keys = keys[:len(keys)-1]
|
||||
}
|
||||
for _, key := range keys {
|
||||
sshKey := models.SSHKey{
|
||||
Title: "Added from " + user.Provider,
|
||||
Content: key,
|
||||
User: *userDB,
|
||||
}
|
||||
|
||||
if err = sshKey.Create(); err != nil {
|
||||
addFlash(ctx, "Could not create ssh key", "error")
|
||||
log.Error().Err(err).Msg("Could not create ssh key")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sess := getSession(ctx)
|
||||
sess.Values["user"] = userDB.ID
|
||||
saveSession(sess, ctx)
|
||||
deleteCsrfCookie(ctx)
|
||||
|
||||
return redirect(ctx, "/")
|
||||
}
|
||||
|
||||
func oauth(ctx echo.Context) error {
|
||||
provider := ctx.Param("provider")
|
||||
|
||||
currUser := getUserLogged(ctx)
|
||||
if currUser != nil {
|
||||
isDelete := false
|
||||
var err error
|
||||
switch provider {
|
||||
case "github":
|
||||
if currUser.GithubID != "" {
|
||||
isDelete = true
|
||||
err = currUser.DeleteProvider(provider)
|
||||
}
|
||||
case "gitea":
|
||||
if currUser.GiteaID != "" {
|
||||
isDelete = true
|
||||
err = currUser.DeleteProvider(provider)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errorRes(500, "Cannot unlink account from "+strings.Title(provider), err)
|
||||
}
|
||||
|
||||
if isDelete {
|
||||
addFlash(ctx, "Account unlinked from "+strings.Title(provider), "success")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
}
|
||||
|
||||
ctxValue := context.WithValue(ctx.Request().Context(), "provider", provider)
|
||||
ctx.SetRequest(ctx.Request().WithContext(ctxValue))
|
||||
if provider != "github" && provider != "gitea" {
|
||||
return errorRes(400, "Unsupported provider", nil)
|
||||
}
|
||||
|
||||
gothic.BeginAuthHandler(ctx.Response(), ctx.Request())
|
||||
return nil
|
||||
}
|
||||
|
||||
func logout(ctx echo.Context) error {
|
||||
deleteSession(ctx)
|
||||
deleteCsrfCookie(ctx)
|
||||
|
@ -2,8 +2,6 @@ package web
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"golang.org/x/crypto/ssh"
|
||||
@ -76,15 +74,12 @@ func sshKeysProcess(ctx echo.Context) error {
|
||||
|
||||
key.UserID = user.ID
|
||||
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
|
||||
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
|
||||
if err != nil {
|
||||
addFlash(ctx, "Invalid SSH key", "error")
|
||||
return redirect(ctx, "/settings")
|
||||
}
|
||||
|
||||
sha := sha256.Sum256(pubKey.Marshal())
|
||||
key.SHA = base64.StdEncoding.EncodeToString(sha[:])
|
||||
|
||||
if err := key.Create(); err != nil {
|
||||
return errorRes(500, "Cannot add SSH key", err)
|
||||
}
|
||||
|
@ -8,6 +8,10 @@ import (
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/markbates/goth/providers/gitea"
|
||||
"github.com/markbates/goth/providers/github"
|
||||
"github.com/rs/zerolog/log"
|
||||
"html/template"
|
||||
"io"
|
||||
@ -96,6 +100,17 @@ func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Con
|
||||
|
||||
func Start() {
|
||||
store = sessions.NewCookieStore([]byte("opengist"))
|
||||
gothic.Store = store
|
||||
goth.UseProviders(
|
||||
github.New("d92c7e165383b2804407", "ffc450216b9776a752cdb0e533f953f65ce632a3", "http://localhost:6157/oauth/github/callback"),
|
||||
gitea.NewCustomisedURL(
|
||||
"efdd0fed-6972-42ce-8f9f-e65b9fd0ca09",
|
||||
"gto_dwilh6ia4nic4f4dt5owv4h7rvuss5ajw2ctqqa44xcpwevyg6wq",
|
||||
"http://localhost:6157/oauth/gitea/callback",
|
||||
"http://localhost:3000/login/oauth/authorize",
|
||||
"http://localhost:3000/login/oauth/access_token",
|
||||
"http://localhost:3000/api/v1/user"),
|
||||
)
|
||||
assetsFS := echo.MustSubFS(EmbedFS, "public/assets")
|
||||
|
||||
e := echo.New()
|
||||
@ -166,6 +181,8 @@ func Start() {
|
||||
g1.GET("/login", login)
|
||||
g1.POST("/login", processLogin)
|
||||
g1.GET("/logout", logout)
|
||||
g1.GET("/oauth/:provider", oauth)
|
||||
g1.GET("/oauth/:provider/callback", oauthCallback)
|
||||
|
||||
g1.GET("/settings", userSettings, logged)
|
||||
g1.POST("/settings/email", emailProcess, logged)
|
||||
|
Reference in New Issue
Block a user