package api import ( domainAPIKey "RenewCMS/internal/domain/apikey" domainArticle "RenewCMS/internal/domain/article" domainImage "RenewCMS/internal/domain/image" domainInvite "RenewCMS/internal/domain/invite" domainMail "RenewCMS/internal/domain/mail" domainUpdate "RenewCMS/internal/domain/update" domainUser "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/config" "RenewCMS/internal/infrastructure/hashing" "RenewCMS/internal/infrastructure/mailer" "RenewCMS/internal/infrastructure/markdown" "RenewCMS/internal/infrastructure/persistence/models" persistence "RenewCMS/internal/infrastructure/persistence/repositories" "RenewCMS/internal/infrastructure/update" "RenewCMS/internal/infrastructure/useCases" "crypto/ed25519" "fmt" "log" "net/http" "os" "path/filepath" "runtime" "sync" "time" "github.com/glebarez/sqlite" "github.com/go-chi/jwtauth/v5" "gorm.io/gorm" ) type UseCases struct { apiKeyAuthOnce sync.Once apiKeyAuthMiddleware func(http.Handler) http.Handler Token *jwtauth.JWTAuth CreateArticleUseCase *useCases.CreateArticleUseCase GetArticleUseCase *useCases.GetArticleUseCase ListArticlesUseCase *useCases.ListArticlesUseCase UpdateArticleUseCase *useCases.UpdateArticleUseCase DeleteArticleUseCase *useCases.DeleteArticleUseCase GetUserUseCase *useCases.GetUserUseCase CreateUserUseCase *useCases.CreateUserUseCase AuthenticateUserUseCase *useCases.AuthenticateUserUseCase VerifyUserUseCase *useCases.VerifyUserUseCase CancelRegistrationUseCase *useCases.CancelRegistrationUseCase RequestPasswordResetUseCase *useCases.RequestPasswordResetUseCase ResetPasswordUseCase *useCases.ResetPasswordUseCase UpdateUserProfileUseCase *useCases.UpdateUserProfileUseCase ListUsersUseCase *useCases.ListUsersUseCase CreateInviteUseCase *useCases.CreateInviteUseCase CancelInviteUseCase *useCases.CancelInviteUseCase RenewInviteUseCase *useCases.RenewInviteUseCase ListUsersAndInvitesUseCase *useCases.ListUsersAndInvitesUseCase GetInviteUseCase *useCases.GetInviteUseCase AcceptInviteUseCase *useCases.AcceptInviteUseCase SendMailUseCase *useCases.SendMailUseCase CreateImageUseCase *useCases.CreateImageUseCase ListMediaUseCase *useCases.ListMediaUseCase RenameMediaUseCase *useCases.RenameMediaUseCase DeleteMediaUseCase *useCases.DeleteMediaUseCase MergeMediaUseCase *useCases.MergeMediaUseCase GetMediaUsageUseCase *useCases.GetMediaUsageUseCase GetSettingsUseCase *useCases.GetSettingsUseCase GetUpdateStatusUseCase *useCases.GetUpdateStatusUseCase CheckForUpdateUseCase *useCases.CheckForUpdateUseCase StartUpdateDownloadUseCase *useCases.StartUpdateDownloadUseCase RequestRestartUseCase *useCases.RequestRestartUseCase RecordRunningInfoUseCase *useCases.RecordRunningInfoUseCase CreateAPIKeyUseCase *useCases.CreateAPIKeyUseCase ListAPIKeysUseCase *useCases.ListAPIKeysUseCase RevokeAPIKeyUseCase *useCases.RevokeAPIKeyUseCase AuthenticateAPIKeyUseCase *useCases.AuthenticateAPIKeyUseCase } type Repositories struct { ArticleRepo domainArticle.Repository UserRepo domainUser.Repository ImageRepo domainImage.Repository InviteRepo domainInvite.Repository APIKeyRepo domainAPIKey.Repository MailRepo domainMail.Repository ReleaseFeed domainUpdate.ReleaseFeed UpdateStore domainUpdate.StatusStore UpdateJobs domainUpdate.JobStore UpdateHelper domainUpdate.HelperDetector AssetFetcher domainUpdate.AssetFetcher SignatureVerifier domainUpdate.SignatureVerifier StagedRunner domainUpdate.StagedBinaryRunner DatabaseBackup domainUpdate.DatabaseBackup UpdateResults domainUpdate.ResultStore RestartRequests domainUpdate.RestartRequestStore RunningInfo domainUpdate.RunningInfoStore } const ( sqliteBusyTimeoutMs = 5000 updateStatusFileName = "update-status.json" updateJobFileName = "update-job.json" updateHelperFileName = "update-helper.json" updatesDirName = "updates" backupsDirName = "backups" ) func SQLiteDSN(dbName string) string { return fmt.Sprintf("%s?_pragma=busy_timeout(%d)", dbName, sqliteBusyTimeoutMs) } func getDb(dbName string, uploadDir string) *gorm.DB { if err := os.MkdirAll(filepath.Dir(dbName), os.ModePerm); err != nil { panic("Unable to create necessary subdirectories: " + err.Error()) } db, err := gorm.Open(sqlite.Open(SQLiteDSN(dbName)), &gorm.Config{}) if err != nil { panic("Unable to open the database: " + err.Error()) } if err := db.AutoMigrate(&models.Article{}, &models.Image{}, &models.User{}, &models.Invite{}, &models.APIKey{}); err != nil { panic("Failed to migrate database: " + err.Error()) } if err := persistence.BackfillUserRoles(db); err != nil { panic("Failed to backfill user roles: " + err.Error()) } if err := persistence.BackfillImageMetadata(db, uploadDir); err != nil { panic("Failed to backfill image metadata: " + err.Error()) } return db } func releasePublicKey() ed25519.PublicKey { key, err := update.EmbeddedPublicKey() if err != nil { panic("Unable to read the embedded release key: " + err.Error()) } return key } func getRepositories(db *gorm.DB, settings config.Settings) *Repositories { dataDir := filepath.Dir(settings.DBFile) return &Repositories{ ArticleRepo: persistence.NewArticleRepository(db), UserRepo: persistence.NewUserRepository(db), ImageRepo: persistence.NewImageRepository(db, settings.UploadDir), InviteRepo: persistence.NewInviteRepository(db), APIKeyRepo: persistence.NewAPIKeyRepository(db), MailRepo: mailer.NewMailRepository(settings.Smtp), ReleaseFeed: update.NewGiteaReleaseFeed(settings.UpdateFeedURL), UpdateStore: update.NewJSONStatusStore(filepath.Join(dataDir, updateStatusFileName)), UpdateJobs: update.NewJSONJobStore(filepath.Join(dataDir, updateJobFileName)), UpdateHelper: update.NewContainerHelperDetector(update.NewFileHelperDetector(filepath.Join(dataDir, updateHelperFileName)), settings.Container), AssetFetcher: update.NewHTTPAssetFetcher(settings.UpdateFeedURL), SignatureVerifier: update.NewEd25519SignatureVerifier(releasePublicKey()), StagedRunner: update.NewExecStagedBinaryRunner(), DatabaseBackup: update.NewSQLiteDatabaseBackup(db, filepath.Join(dataDir, backupsDirName), time.Now), UpdateResults: update.NewJSONResultStore(filepath.Join(dataDir, domainUpdate.ResultFileName)), RestartRequests: update.NewJSONRestartRequestStore(filepath.Join(dataDir, domainUpdate.RestartRequestFileName)), RunningInfo: update.NewJSONRunningInfoStore(filepath.Join(dataDir, domainUpdate.RunningInfoFileName)), } } func getUseCases(repos *Repositories, settings config.Settings) *UseCases { hasher := hashing.NewBcryptHasher() renderer := markdown.NewConverter() keyGenerator := hashing.NewSHA256KeyGenerator() return &UseCases{ Token: jwtauth.New("HS256", []byte(settings.JWTSecret), nil), CreateArticleUseCase: useCases.NewCreateArticleUseCase(repos.ArticleRepo, renderer), GetArticleUseCase: useCases.NewGetArticleUseCase(repos.ArticleRepo, renderer), ListArticlesUseCase: useCases.NewListArticlesUseCase(repos.ArticleRepo, renderer), UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(repos.ArticleRepo, renderer), DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(repos.ArticleRepo), GetUserUseCase: useCases.NewGetUserUseCase(repos.UserRepo), CreateUserUseCase: useCases.NewCreateUserUseCase(repos.UserRepo, hasher, repos.MailRepo, settings.PublicURL), AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(repos.UserRepo, hasher), VerifyUserUseCase: useCases.NewVerifyUserUseCase(repos.UserRepo, hasher), CancelRegistrationUseCase: useCases.NewCancelRegistrationUseCase(repos.UserRepo), RequestPasswordResetUseCase: useCases.NewRequestPasswordResetUseCase(repos.UserRepo, hasher, repos.MailRepo, settings.PublicURL), ResetPasswordUseCase: useCases.NewResetPasswordUseCase(repos.UserRepo, hasher, repos.APIKeyRepo, time.Now), UpdateUserProfileUseCase: useCases.NewUpdateUserProfileUseCase(repos.UserRepo), ListUsersUseCase: useCases.NewListUsersUseCase(repos.UserRepo), CreateInviteUseCase: useCases.NewCreateInviteUseCase(repos.UserRepo, repos.InviteRepo, hasher, repos.MailRepo, settings.PublicURL), CancelInviteUseCase: useCases.NewCancelInviteUseCase(repos.InviteRepo), RenewInviteUseCase: useCases.NewRenewInviteUseCase(repos.UserRepo, repos.InviteRepo, hasher, repos.MailRepo, settings.PublicURL), ListUsersAndInvitesUseCase: useCases.NewListUsersAndInvitesUseCase(repos.UserRepo, repos.InviteRepo), GetInviteUseCase: useCases.NewGetInviteUseCase(repos.InviteRepo, hasher), AcceptInviteUseCase: useCases.NewAcceptInviteUseCase(repos.UserRepo, repos.InviteRepo, hasher), SendMailUseCase: useCases.NewSendMailUseCase(repos.MailRepo), CreateImageUseCase: useCases.NewCreateImageUseCase(repos.ImageRepo), ListMediaUseCase: useCases.NewListMediaUseCase(repos.ImageRepo, repos.ArticleRepo, repos.UserRepo), RenameMediaUseCase: useCases.NewRenameMediaUseCase(repos.ImageRepo), DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(repos.ImageRepo), MergeMediaUseCase: useCases.NewMergeMediaUseCase(repos.ImageRepo, repos.ArticleRepo), GetMediaUsageUseCase: useCases.NewGetMediaUsageUseCase(repos.ImageRepo, repos.ArticleRepo), GetSettingsUseCase: useCases.NewGetSettingsUseCase(settings.Version), GetUpdateStatusUseCase: useCases.NewGetUpdateStatusUseCase(repos.UpdateStore, repos.UpdateJobs, repos.UpdateHelper, repos.UpdateResults, repos.RestartRequests, settings.Version, settings.Container, time.Now), CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(repos.ReleaseFeed, repos.UpdateStore, settings.Version, time.Now), StartUpdateDownloadUseCase: startUpdateDownloadUseCase(repos, settings), RequestRestartUseCase: useCases.NewRequestRestartUseCase(repos.UpdateStore, repos.UpdateJobs, repos.UpdateHelper, repos.UpdateResults, repos.RestartRequests, settings.Version, time.Now), RecordRunningInfoUseCase: useCases.NewRecordRunningInfoUseCase(repos.RunningInfo, settings.Version, os.Getpid(), time.Now), CreateAPIKeyUseCase: useCases.NewCreateAPIKeyUseCase(repos.APIKeyRepo, keyGenerator, time.Now), ListAPIKeysUseCase: useCases.NewListAPIKeysUseCase(repos.APIKeyRepo, repos.UserRepo, time.Now), RevokeAPIKeyUseCase: useCases.NewRevokeAPIKeyUseCase(repos.APIKeyRepo, repos.UserRepo, time.Now), AuthenticateAPIKeyUseCase: useCases.NewAuthenticateAPIKeyUseCase(repos.APIKeyRepo, keyGenerator, repos.UserRepo, time.Now), } } func startUpdateDownloadUseCase(repos *Repositories, settings config.Settings) *useCases.StartUpdateDownloadUseCase { return useCases.NewStartUpdateDownloadUseCase(useCases.StartUpdateDownloadDependencies{ Store: repos.UpdateStore, Jobs: repos.UpdateJobs, Helper: repos.UpdateHelper, Results: repos.UpdateResults, Requests: repos.RestartRequests, Fetcher: repos.AssetFetcher, Verifier: repos.SignatureVerifier, Runner: repos.StagedRunner, Backup: repos.DatabaseBackup, Staging: update.NewOSStagingArea(filepath.Join(filepath.Dir(settings.DBFile), updatesDirName)), Logger: log.Default(), CurrentVersion: settings.Version, Platform: domainUpdate.Platform{OS: runtime.GOOS, Arch: runtime.GOARCH}, FeedURL: settings.UpdateFeedURL, Now: time.Now, }) } func InitContainer(settings config.Settings) (*UseCases, func() error) { db := getDb(settings.DBFile, settings.UploadDir) repos := getRepositories(db, settings) return getUseCases(repos, settings), closeDb(db) } func closeDb(db *gorm.DB) func() error { return func() error { sqlDb, err := db.DB() if err != nil { return err } return sqlDb.Close() } } type BrowserUpdateCommands struct { Enable *useCases.EnableBrowserUpdatesUseCase Disable *useCases.DisableBrowserUpdatesUseCase } func InitBrowserUpdateCommands() *BrowserUpdateCommands { units := update.NewSystemdUnits() dependencies := useCases.ManageBrowserUpdatesDependencies{ Probe: update.NewSystemMachineProbe(), Locator: units, Units: units, Manager: update.NewSystemctlControl(domainUpdate.ServiceUnitName), Status: update.NewJSONHelperStatusWriter(), Now: time.Now, } return &BrowserUpdateCommands{ Enable: useCases.NewEnableBrowserUpdatesUseCase(dependencies), Disable: useCases.NewDisableBrowserUpdatesUseCase(dependencies), } } func InitApplyUpdateCommand(options domainUpdate.InstallOptions) *useCases.ApplyRestartRequestUseCase { layout := domainUpdate.HelperLayout{DataDir: options.DataDir, BinaryPath: options.Binary} files := update.NewNoFollowFilesystem() return useCases.NewApplyRestartRequestUseCase(useCases.ApplyRestartRequestDependencies{ Requests: update.NewFSRestartRequestStore(files, layout.RestartRequestPath()), Results: update.NewFSResultStore(files, layout.ResultPath()), Running: update.NewJSONRunningInfoStore(filepath.Join(options.DataDir, domainUpdate.RunningInfoFileName)), Files: files, Service: update.NewSystemctlControl(options.Service), Health: update.NewHTTPHealthChecker(options.Port), Verifier: update.NewEd25519SignatureVerifier(releasePublicKey()), Clock: update.NewSystemClock(), Logger: log.Default(), Layout: layout, Platform: domainUpdate.Platform{OS: runtime.GOOS, Arch: runtime.GOARCH}, }) }