From 4eb18388db1ac5bf6f9259a5144638b1d5121f63 Mon Sep 17 00:00:00 2001 From: Joakim Date: Tue, 6 Jan 2026 15:42:03 +0100 Subject: [PATCH] feat(backend): add OAuth2/JWT authentication support - Add OAuth2 client for Authentik integration - Implement JWT token generation and validation - Add refresh token support with database storage - Update database schema with oauth_subject, oauth_provider, and refresh_tokens table - Create auth package with config, jwt, oauth, and token management - Add OAuth endpoints: /auth/login, /auth/callback, /auth/refresh, /auth/logout - Update AuthMiddleware to support both JWT and API key authentication - Add user helper functions for OAuth user creation and retrieval - Add .env.example with OAuth configuration template API keys still work for CLI compatibility while JWT tokens support web/mobile clients. --- opal-task/.env.example | 19 ++ opal-task/go.mod | 2 + opal-task/go.sum | 4 + opal-task/internal/api/handlers/oauth.go | 167 +++++++++++++++++ opal-task/internal/api/middleware.go | 25 ++- opal-task/internal/api/server.go | 6 + opal-task/internal/auth/config.go | 41 ++++ opal-task/internal/auth/jwt.go | 57 ++++++ opal-task/internal/auth/oauth.go | 74 ++++++++ opal-task/internal/auth/tokens.go | 91 +++++++++ opal-task/internal/engine/database.go | 16 ++ opal-task/internal/engine/users.go | 84 +++++++++ opal-web/.gitignore | 23 +++ opal-web/.npmrc | 1 + opal-web/README.md | 38 ++++ opal-web/bun.lock | 227 +++++++++++++++++++++++ opal-web/jsconfig.json | 19 ++ opal-web/package.json | 23 +++ opal-web/src/app.d.ts | 13 ++ opal-web/src/app.html | 11 ++ opal-web/src/lib/assets/favicon.svg | 1 + opal-web/src/lib/index.js | 1 + opal-web/src/routes/+layout.svelte | 11 ++ opal-web/src/routes/+page.svelte | 2 + opal-web/static/robots.txt | 3 + opal-web/svelte.config.js | 6 + opal-web/vite.config.js | 6 + 27 files changed, 965 insertions(+), 6 deletions(-) create mode 100644 opal-task/.env.example create mode 100644 opal-task/internal/api/handlers/oauth.go create mode 100644 opal-task/internal/auth/config.go create mode 100644 opal-task/internal/auth/jwt.go create mode 100644 opal-task/internal/auth/oauth.go create mode 100644 opal-task/internal/auth/tokens.go create mode 100644 opal-task/internal/engine/users.go create mode 100644 opal-web/.gitignore create mode 100644 opal-web/.npmrc create mode 100644 opal-web/README.md create mode 100644 opal-web/bun.lock create mode 100644 opal-web/jsconfig.json create mode 100644 opal-web/package.json create mode 100644 opal-web/src/app.d.ts create mode 100644 opal-web/src/app.html create mode 100644 opal-web/src/lib/assets/favicon.svg create mode 100644 opal-web/src/lib/index.js create mode 100644 opal-web/src/routes/+layout.svelte create mode 100644 opal-web/src/routes/+page.svelte create mode 100644 opal-web/static/robots.txt create mode 100644 opal-web/svelte.config.js create mode 100644 opal-web/vite.config.js diff --git a/opal-task/.env.example b/opal-task/.env.example new file mode 100644 index 0000000..fe4f148 --- /dev/null +++ b/opal-task/.env.example @@ -0,0 +1,19 @@ +# Server Configuration +SERVER_ADDR=:8080 + +# Database +OPAL_DB_PATH=/var/lib/opal/opal.db + +# OAuth2 / Authentik +OAUTH_ENABLED=true +OAUTH_ISSUER=https://auth.example.com/application/o/opal/ +OAUTH_CLIENT_ID=your_client_id_here +OAUTH_CLIENT_SECRET=your_client_secret_here +OAUTH_REDIRECT_URI=https://opal.example.com/auth/callback + +# JWT Configuration +JWT_SECRET=generate_random_secret_with_openssl_rand_hex_32 +JWT_EXPIRY=3600 + +# Refresh Token Configuration +REFRESH_TOKEN_EXPIRY=604800 diff --git a/opal-task/go.mod b/opal-task/go.mod index 92a89f5..75e954b 100644 --- a/opal-task/go.mod +++ b/opal-task/go.mod @@ -16,6 +16,7 @@ require ( require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -29,6 +30,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect diff --git a/opal-task/go.sum b/opal-task/go.sum index bc7e885..54e35eb 100644 --- a/opal-task/go.sum +++ b/opal-task/go.sum @@ -11,6 +11,8 @@ github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -65,6 +67,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= diff --git a/opal-task/internal/api/handlers/oauth.go b/opal-task/internal/api/handlers/oauth.go new file mode 100644 index 0000000..3dcd5bf --- /dev/null +++ b/opal-task/internal/api/handlers/oauth.go @@ -0,0 +1,167 @@ +package handlers + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "net/http" + "time" + + "git.jnss.me/joakim/opal/internal/auth" + "git.jnss.me/joakim/opal/internal/engine" +) + +var authConfig = auth.LoadConfig() +var oauthClient *auth.OAuthClient + +func init() { + if authConfig.OAuthEnabled { + oauthClient = auth.NewOAuthClient(authConfig) + } +} + +// GetLoginURL returns the OAuth authorization URL +func GetLoginURL(w http.ResponseWriter, r *http.Request) { + if !authConfig.OAuthEnabled { + errorResponse(w, http.StatusNotImplemented, "OAuth not enabled") + return + } + + state := generateState() + url := oauthClient.GetAuthURL(state) + + jsonResponse(w, http.StatusOK, map[string]string{ + "url": url, + "state": state, + }) +} + +// OAuthCallback handles the OAuth callback +func OAuthCallback(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + if code == "" { + errorResponse(w, http.StatusBadRequest, "missing code parameter") + return + } + + // Exchange code for token + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + oauthToken, err := oauthClient.ExchangeCode(ctx, code) + if err != nil { + errorResponse(w, http.StatusInternalServerError, "failed to exchange code: "+err.Error()) + return + } + + // Get user info + userInfo, err := oauthClient.GetUserInfo(ctx, oauthToken.AccessToken) + if err != nil { + errorResponse(w, http.StatusInternalServerError, "failed to get user info: "+err.Error()) + return + } + + // Find or create user + user, err := engine.FindOrCreateOAuthUser(userInfo.Sub, userInfo.Username, userInfo.Email) + if err != nil { + errorResponse(w, http.StatusInternalServerError, "failed to create user: "+err.Error()) + return + } + + // Generate JWT + accessToken, expiresAt, err := auth.GenerateJWT(user.ID, user.Username, user.Email, authConfig) + if err != nil { + errorResponse(w, http.StatusInternalServerError, "failed to generate token: "+err.Error()) + return + } + + // Generate refresh token + refreshToken, err := auth.GenerateRefreshToken() + if err != nil { + errorResponse(w, http.StatusInternalServerError, "failed to generate refresh token: "+err.Error()) + return + } + + // Store refresh token + if err := auth.StoreRefreshToken(user.ID, refreshToken, authConfig.RefreshTokenExpiry); err != nil { + errorResponse(w, http.StatusInternalServerError, "failed to store refresh token: "+err.Error()) + return + } + + jsonResponse(w, http.StatusOK, map[string]interface{}{ + "access_token": accessToken, + "refresh_token": refreshToken, + "expires_at": expiresAt, + "token_type": "Bearer", + "user": map[string]interface{}{ + "id": user.ID, + "username": user.Username, + "email": user.Email, + }, + }) +} + +// RefreshToken handles token refresh +func RefreshToken(w http.ResponseWriter, r *http.Request) { + var req struct { + RefreshToken string `json:"refresh_token"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + errorResponse(w, http.StatusBadRequest, "invalid request") + return + } + + // Validate refresh token + userID, err := auth.ValidateRefreshToken(req.RefreshToken) + if err != nil { + errorResponse(w, http.StatusUnauthorized, "invalid refresh token: "+err.Error()) + return + } + + // Get user + user, err := engine.GetUser(userID) + if err != nil { + errorResponse(w, http.StatusNotFound, "user not found") + return + } + + // Generate new access token + accessToken, expiresAt, err := auth.GenerateJWT(user.ID, user.Username, user.Email, authConfig) + if err != nil { + errorResponse(w, http.StatusInternalServerError, "failed to generate token") + return + } + + jsonResponse(w, http.StatusOK, map[string]interface{}{ + "access_token": accessToken, + "expires_at": expiresAt, + "token_type": "Bearer", + }) +} + +// Logout revokes refresh token +func Logout(w http.ResponseWriter, r *http.Request) { + var req struct { + RefreshToken string `json:"refresh_token"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + errorResponse(w, http.StatusBadRequest, "invalid request") + return + } + + if err := auth.RevokeRefreshToken(req.RefreshToken); err != nil { + errorResponse(w, http.StatusInternalServerError, "failed to revoke token") + return + } + + jsonResponse(w, http.StatusOK, map[string]string{"message": "logged out"}) +} + +func generateState() string { + b := make([]byte, 16) + rand.Read(b) + return base64.URLEncoding.EncodeToString(b) +} diff --git a/opal-task/internal/api/middleware.go b/opal-task/internal/api/middleware.go index 0a6ffd2..7e6d935 100644 --- a/opal-task/internal/api/middleware.go +++ b/opal-task/internal/api/middleware.go @@ -5,6 +5,7 @@ import ( "net/http" "strings" + "git.jnss.me/joakim/opal/internal/auth" "git.jnss.me/joakim/opal/internal/engine" ) @@ -12,8 +13,10 @@ type contextKey string const userIDKey contextKey = "userID" -// AuthMiddleware validates API keys and adds userID to context +// AuthMiddleware validates JWT tokens or API keys and adds userID to context func AuthMiddleware() func(http.Handler) http.Handler { + authCfg := auth.LoadConfig() + return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Get Authorization header @@ -30,17 +33,27 @@ func AuthMiddleware() func(http.Handler) http.Handler { return } - apiKey := parts[1] + token := parts[1] - // Validate API key - valid, userID, err := engine.ValidateAPIKey(apiKey) + // Try JWT first if OAuth is enabled + if authCfg.OAuthEnabled { + if claims, err := auth.ValidateJWT(token, authCfg); err == nil { + // Valid JWT - add userID to context + ctx := context.WithValue(r.Context(), userIDKey, claims.UserID) + next.ServeHTTP(w, r.WithContext(ctx)) + return + } + } + + // Fall back to API key validation (for CLI compatibility) + valid, userID, err := engine.ValidateAPIKey(token) if err != nil { - Error(w, http.StatusInternalServerError, "failed to validate API key") + Error(w, http.StatusInternalServerError, "failed to validate credentials") return } if !valid { - Error(w, http.StatusUnauthorized, "invalid API key") + Error(w, http.StatusUnauthorized, "invalid credentials") return } diff --git a/opal-task/internal/api/server.go b/opal-task/internal/api/server.go index c14f8c7..a177166 100644 --- a/opal-task/internal/api/server.go +++ b/opal-task/internal/api/server.go @@ -39,6 +39,12 @@ func (s *Server) setupRoutes() { JSON(w, http.StatusOK, map[string]string{"status": "ok"}) }) + // OAuth endpoints (no auth required) + r.Get("/auth/login", handlers.GetLoginURL) + r.Post("/auth/callback", handlers.OAuthCallback) + r.Post("/auth/refresh", handlers.RefreshToken) + r.Post("/auth/logout", handlers.Logout) + // Protected routes r.Group(func(r chi.Router) { r.Use(AuthMiddleware()) diff --git a/opal-task/internal/auth/config.go b/opal-task/internal/auth/config.go new file mode 100644 index 0000000..face3ba --- /dev/null +++ b/opal-task/internal/auth/config.go @@ -0,0 +1,41 @@ +package auth + +import ( + "os" + "strconv" +) + +type Config struct { + OAuthEnabled bool + OAuthIssuer string + OAuthClientID string + OAuthClientSecret string + OAuthRedirectURI string + JWTSecret []byte + JWTExpiry int + RefreshTokenExpiry int +} + +func LoadConfig() *Config { + enabled, _ := strconv.ParseBool(getEnv("OAUTH_ENABLED", "false")) + jwtExpiry, _ := strconv.Atoi(getEnv("JWT_EXPIRY", "3600")) + refreshExpiry, _ := strconv.Atoi(getEnv("REFRESH_TOKEN_EXPIRY", "604800")) + + return &Config{ + OAuthEnabled: enabled, + OAuthIssuer: getEnv("OAUTH_ISSUER", ""), + OAuthClientID: getEnv("OAUTH_CLIENT_ID", ""), + OAuthClientSecret: getEnv("OAUTH_CLIENT_SECRET", ""), + OAuthRedirectURI: getEnv("OAUTH_REDIRECT_URI", ""), + JWTSecret: []byte(getEnv("JWT_SECRET", "change-me-in-production")), + JWTExpiry: jwtExpiry, + RefreshTokenExpiry: refreshExpiry, + } +} + +func getEnv(key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + return fallback +} diff --git a/opal-task/internal/auth/jwt.go b/opal-task/internal/auth/jwt.go new file mode 100644 index 0000000..c17687f --- /dev/null +++ b/opal-task/internal/auth/jwt.go @@ -0,0 +1,57 @@ +package auth + +import ( + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type Claims struct { + UserID int `json:"user_id"` + Username string `json:"username"` + Email string `json:"email,omitempty"` + jwt.RegisteredClaims +} + +func GenerateJWT(userID int, username, email string, cfg *Config) (string, int64, error) { + expiresAt := time.Now().Add(time.Duration(cfg.JWTExpiry) * time.Second) + + claims := &Claims{ + UserID: userID, + Username: username, + Email: email, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expiresAt), + IssuedAt: jwt.NewNumericDate(time.Now()), + Issuer: "opal-task", + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + signedToken, err := token.SignedString(cfg.JWTSecret) + if err != nil { + return "", 0, err + } + + return signedToken, expiresAt.Unix(), nil +} + +func ValidateJWT(tokenString string, cfg *Config) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return cfg.JWTSecret, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, fmt.Errorf("invalid token") +} diff --git a/opal-task/internal/auth/oauth.go b/opal-task/internal/auth/oauth.go new file mode 100644 index 0000000..fb448b1 --- /dev/null +++ b/opal-task/internal/auth/oauth.go @@ -0,0 +1,74 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "golang.org/x/oauth2" +) + +type OAuthClient struct { + config *oauth2.Config + cfg *Config +} + +func NewOAuthClient(cfg *Config) *OAuthClient { + return &OAuthClient{ + config: &oauth2.Config{ + ClientID: cfg.OAuthClientID, + ClientSecret: cfg.OAuthClientSecret, + RedirectURL: cfg.OAuthRedirectURI, + Endpoint: oauth2.Endpoint{ + AuthURL: cfg.OAuthIssuer + "../authorize/", + TokenURL: cfg.OAuthIssuer + "../token/", + }, + Scopes: []string{"openid", "profile", "email"}, + }, + cfg: cfg, + } +} + +func (c *OAuthClient) GetAuthURL(state string) string { + return c.config.AuthCodeURL(state) +} + +func (c *OAuthClient) ExchangeCode(ctx context.Context, code string) (*oauth2.Token, error) { + return c.config.Exchange(ctx, code) +} + +type UserInfo struct { + Sub string `json:"sub"` + Username string `json:"preferred_username"` + Email string `json:"email"` + Name string `json:"name"` +} + +func (c *OAuthClient) GetUserInfo(ctx context.Context, accessToken string) (*UserInfo, error) { + req, err := http.NewRequestWithContext(ctx, "GET", c.cfg.OAuthIssuer+"../userinfo/", nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+accessToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("userinfo request failed: %s", string(body)) + } + + var userInfo UserInfo + if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { + return nil, err + } + + return &userInfo, nil +} diff --git a/opal-task/internal/auth/tokens.go b/opal-task/internal/auth/tokens.go new file mode 100644 index 0000000..8af4a68 --- /dev/null +++ b/opal-task/internal/auth/tokens.go @@ -0,0 +1,91 @@ +package auth + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" + "time" + + "git.jnss.me/joakim/opal/internal/engine" +) + +func GenerateRefreshToken() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(b), nil +} + +func HashToken(token string) string { + hash := sha256.Sum256([]byte(token)) + return base64.URLEncoding.EncodeToString(hash[:]) +} + +func StoreRefreshToken(userID int, token string, expiresIn int) error { + db := engine.GetDB() + if db == nil { + return fmt.Errorf("database not initialized") + } + + hash := HashToken(token) + expiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second).Unix() + + _, err := db.Exec(` + INSERT INTO refresh_tokens (user_id, token_hash, expires_at, created_at) + VALUES (?, ?, ?, ?) + `, userID, hash, expiresAt, time.Now().Unix()) + + return err +} + +func ValidateRefreshToken(token string) (int, error) { + db := engine.GetDB() + if db == nil { + return 0, fmt.Errorf("database not initialized") + } + + hash := HashToken(token) + + var userID int + var expiresAt int64 + var revoked int + + err := db.QueryRow(` + SELECT user_id, expires_at, revoked + FROM refresh_tokens + WHERE token_hash = ? + `, hash).Scan(&userID, &expiresAt, &revoked) + + if err != nil { + return 0, err + } + + if revoked == 1 { + return 0, fmt.Errorf("token revoked") + } + + if time.Now().Unix() > expiresAt { + return 0, fmt.Errorf("token expired") + } + + return userID, nil +} + +func RevokeRefreshToken(token string) error { + db := engine.GetDB() + if db == nil { + return fmt.Errorf("database not initialized") + } + + hash := HashToken(token) + + _, err := db.Exec(` + UPDATE refresh_tokens + SET revoked = 1 + WHERE token_hash = ? + `, hash) + + return err +} diff --git a/opal-task/internal/engine/database.go b/opal-task/internal/engine/database.go index 5254c7a..aa6e219 100644 --- a/opal-task/internal/engine/database.go +++ b/opal-task/internal/engine/database.go @@ -132,6 +132,8 @@ func runMigrations() error { id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, email TEXT, + oauth_subject TEXT UNIQUE, + oauth_provider TEXT DEFAULT 'authentik', created_at INTEGER NOT NULL ); @@ -183,6 +185,20 @@ func runMigrations() error { -- Default: keep change log for 30 days INSERT INTO sync_config (key, value) VALUES ('change_log_retention_days', '30'); + -- Refresh tokens for OAuth + CREATE TABLE refresh_tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + token_hash TEXT UNIQUE NOT NULL, + expires_at INTEGER NOT NULL, + created_at INTEGER NOT NULL, + revoked INTEGER DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + + CREATE INDEX idx_refresh_tokens_hash ON refresh_tokens(token_hash); + CREATE INDEX idx_refresh_tokens_user ON refresh_tokens(user_id); + -- Triggers to populate change_log CREATE TRIGGER track_task_create AFTER INSERT ON tasks BEGIN diff --git a/opal-task/internal/engine/users.go b/opal-task/internal/engine/users.go new file mode 100644 index 0000000..717864d --- /dev/null +++ b/opal-task/internal/engine/users.go @@ -0,0 +1,84 @@ +package engine + +import ( + "database/sql" + "fmt" + "time" +) + +type User struct { + ID int + Username string + Email string + OAuthSubject string + OAuthProvider string + CreatedAt int64 +} + +func FindOrCreateOAuthUser(subject, username, email string) (*User, error) { + db := GetDB() + if db == nil { + return nil, fmt.Errorf("database not initialized") + } + + // Try to find existing user + var user User + err := db.QueryRow(` + SELECT id, username, COALESCE(email, ''), COALESCE(oauth_subject, ''), COALESCE(oauth_provider, ''), created_at + FROM users + WHERE oauth_subject = ? + `, subject).Scan(&user.ID, &user.Username, &user.Email, &user.OAuthSubject, &user.OAuthProvider, &user.CreatedAt) + + if err == nil { + // User exists + return &user, nil + } + + if err != sql.ErrNoRows { + return nil, err + } + + // Create new user + result, err := db.Exec(` + INSERT INTO users (username, email, oauth_subject, oauth_provider, created_at) + VALUES (?, ?, ?, ?, ?) + `, username, email, subject, "authentik", time.Now().Unix()) + + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + user.ID = int(id) + user.Username = username + user.Email = email + user.OAuthSubject = subject + user.OAuthProvider = "authentik" + user.CreatedAt = time.Now().Unix() + + return &user, nil +} + +func GetUser(id int) (*User, error) { + db := GetDB() + if db == nil { + return nil, fmt.Errorf("database not initialized") + } + + var user User + err := db.QueryRow(` + SELECT id, username, COALESCE(email, ''), COALESCE(oauth_subject, ''), COALESCE(oauth_provider, ''), created_at + FROM users + WHERE id = ? + `, id).Scan(&user.ID, &user.Username, &user.Email, &user.OAuthSubject, &user.OAuthProvider, &user.CreatedAt) + + if err != nil { + return nil, err + } + + return &user, nil +} diff --git a/opal-web/.gitignore b/opal-web/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/opal-web/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/opal-web/.npmrc b/opal-web/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/opal-web/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/opal-web/README.md b/opal-web/README.md new file mode 100644 index 0000000..75842c4 --- /dev/null +++ b/opal-web/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/opal-web/bun.lock b/opal-web/bun.lock new file mode 100644 index 0000000..3c22669 --- /dev/null +++ b/opal-web/bun.lock @@ -0,0 +1,227 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "opal-web", + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "typescript": "^5.9.3", + "vite": "^7.2.6", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="], + + "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="], + + "@sveltejs/kit": ["@sveltejs/kit@2.49.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-luTmE2Isk9GRJnitqanLoByKBiyLdfLpV2qV9a25JMxjbQt919TVqG8pibJDkxTvX9+w2k/9IL7o+/RtG++3QA=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="], + + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="], + + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "esrap": ["esrap@2.2.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "svelte": ["svelte@5.46.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA=="], + + "svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], + + "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + } +} diff --git a/opal-web/jsconfig.json b/opal-web/jsconfig.json new file mode 100644 index 0000000..0b2d886 --- /dev/null +++ b/opal-web/jsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/opal-web/package.json b/opal-web/package.json new file mode 100644 index 0000000..c674d3f --- /dev/null +++ b/opal-web/package.json @@ -0,0 +1,23 @@ +{ + "name": "opal-web", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "typescript": "^5.9.3", + "vite": "^7.2.6" + } +} diff --git a/opal-web/src/app.d.ts b/opal-web/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/opal-web/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/opal-web/src/app.html b/opal-web/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/opal-web/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/opal-web/src/lib/assets/favicon.svg b/opal-web/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/opal-web/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/opal-web/src/lib/index.js b/opal-web/src/lib/index.js new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/opal-web/src/lib/index.js @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/opal-web/src/routes/+layout.svelte b/opal-web/src/routes/+layout.svelte new file mode 100644 index 0000000..5c4f0f7 --- /dev/null +++ b/opal-web/src/routes/+layout.svelte @@ -0,0 +1,11 @@ + + + + + + +{@render children()} diff --git a/opal-web/src/routes/+page.svelte b/opal-web/src/routes/+page.svelte new file mode 100644 index 0000000..cc88df0 --- /dev/null +++ b/opal-web/src/routes/+page.svelte @@ -0,0 +1,2 @@ +

Welcome to SvelteKit

+

Visit svelte.dev/docs/kit to read the documentation

diff --git a/opal-web/static/robots.txt b/opal-web/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/opal-web/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/opal-web/svelte.config.js b/opal-web/svelte.config.js new file mode 100644 index 0000000..9338c8c --- /dev/null +++ b/opal-web/svelte.config.js @@ -0,0 +1,6 @@ +import adapter from '@sveltejs/adapter-static'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { kit: { adapter: adapter() } }; + +export default config; diff --git a/opal-web/vite.config.js b/opal-web/vite.config.js new file mode 100644 index 0000000..bbf8c7d --- /dev/null +++ b/opal-web/vite.config.js @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +});