feat: full project sync - CI fixes, frontend, workspace API, and all changes

This commit is contained in:
Tomas Dvorak
2026-04-27 09:08:07 +02:00
parent a07fca997e
commit 89b9390c14
109 changed files with 21120 additions and 545 deletions
+75 -2
View File
@@ -6,6 +6,7 @@ import (
"encoding/base64"
"encoding/json"
"excalidraw-complete/core"
"excalidraw-complete/workspace"
"fmt"
"io"
"net/http"
@@ -34,6 +35,7 @@ var (
oidcOauthConfig *oauth2.Config
oidcProvider *oidc.Provider
verifier *oidc.IDTokenVerifier
workspaceStore *workspace.Store
)
// AppClaims represents the custom claims for the JWT.
@@ -48,12 +50,17 @@ type AppClaims struct {
// OIDCClaims represents the claims from OIDC token
type OIDCClaims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
Picture string `json:"picture"`
Sub string `json:"sub"`
}
func SetWorkspaceStore(store *workspace.Store) {
workspaceStore = store
}
func InitAuth() {
oidcConfigured := os.Getenv("OIDC_ISSUER_URL") != "" && os.Getenv("OIDC_CLIENT_ID") != ""
githubConfigured := os.Getenv("GITHUB_CLIENT_ID") != "" && os.Getenv("GITHUB_CLIENT_SECRET") != ""
@@ -160,15 +167,18 @@ func Init() {
}
}
func generateStateOauthCookie(w http.ResponseWriter) string {
func generateStateOauthCookie(w http.ResponseWriter, r *http.Request) string {
b := make([]byte, 16)
rand.Read(b)
state := base64.URLEncoding.EncodeToString(b)
cookie := &http.Cookie{
Name: "oauthstate",
Value: state,
Path: "/",
Expires: time.Now().Add(10 * time.Minute),
HttpOnly: true,
Secure: r.Header.Get("X-Forwarded-Proto") == "https",
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(w, cookie)
return state
@@ -179,7 +189,7 @@ func HandleGitHubLogin(w http.ResponseWriter, r *http.Request) {
http.Error(w, "GitHub OAuth is not configured", http.StatusInternalServerError)
return
}
state := generateStateOauthCookie(w)
state := generateStateOauthCookie(w, r)
url := githubOauthConfig.AuthCodeURL(state)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
@@ -189,6 +199,11 @@ func HandleGitHubCallback(w http.ResponseWriter, r *http.Request) {
http.Error(w, "GitHub OAuth is not configured", http.StatusInternalServerError)
return
}
if !validateStateCookie(r, "oauthstate") {
logrus.Warn("invalid github oauth state")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
token, err := githubOauthConfig.Exchange(context.Background(), r.FormValue("code"))
if err != nil {
@@ -216,6 +231,7 @@ func HandleGitHubCallback(w http.ResponseWriter, r *http.Request) {
var githubUser struct {
ID int64 `json:"id"`
Login string `json:"login"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
Name string `json:"name"`
}
@@ -226,6 +242,26 @@ func HandleGitHubCallback(w http.ResponseWriter, r *http.Request) {
return
}
if workspaceStore != nil {
_, session, sessionToken, err := workspaceStore.UpsertOAuthSession(r.Context(), workspace.OAuthProfile{
Provider: "github",
ProviderUserID: fmt.Sprintf("%d", githubUser.ID),
Email: githubUser.Email,
Name: githubUser.Name,
Username: githubUser.Login,
AvatarURL: githubUser.AvatarURL,
EmailVerified: githubUser.Email != "",
})
if err != nil {
logrus.Errorf("failed to create workspace oauth session: %s", err.Error())
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
workspace.SetSessionCookie(w, r, sessionToken, session.ExpiresAt)
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
// Create user object using Subject instead of GitHubID
user := &core.User{
Subject: fmt.Sprintf("github:%d", githubUser.ID),
@@ -280,6 +316,11 @@ func HandleOIDCCallback(w http.ResponseWriter, r *http.Request) {
http.Error(w, "OIDC is not configured", http.StatusInternalServerError)
return
}
if !validateStateCookie(r, "oidc_state") {
logrus.Warn("invalid oidc state")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
code := r.FormValue("code")
if code == "" {
@@ -330,6 +371,26 @@ func HandleOIDCCallback(w http.ResponseWriter, r *http.Request) {
user.Login = user.Email
}
if workspaceStore != nil {
_, session, sessionToken, err := workspaceStore.UpsertOAuthSession(r.Context(), workspace.OAuthProfile{
Provider: "oidc",
ProviderUserID: claims.Sub,
Email: claims.Email,
Name: claims.Name,
Username: user.Login,
AvatarURL: claims.Picture,
EmailVerified: claims.EmailVerified,
})
if err != nil {
logrus.Errorf("failed to create workspace oidc session: %s", err.Error())
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
workspace.SetSessionCookie(w, r, sessionToken, session.ExpiresAt)
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
jwtToken, err := createJWT(user)
if err != nil {
logrus.Errorf("failed to create JWT: %s", err.Error())
@@ -341,6 +402,18 @@ func HandleOIDCCallback(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, fmt.Sprintf("/?token=%s", jwtToken), http.StatusTemporaryRedirect)
}
func validateStateCookie(r *http.Request, name string) bool {
state := r.FormValue("state")
if state == "" {
return false
}
cookie, err := r.Cookie(name)
if err != nil || cookie.Value == "" {
return false
}
return cookie.Value == state
}
func createJWT(user *core.User) (string, error) {
claims := AppClaims{
RegisteredClaims: jwt.RegisteredClaims{