├── .dockerignore ├── .env.example ├── .env.test ├── .env.test.basic ├── .gitignore ├── .goreleaser.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── apis ├── admin.go ├── admin_test.go ├── api_error.go ├── api_error_test.go ├── backup.go ├── backup_test.go ├── base.go ├── base_test.go ├── collection.go ├── collection_test.go ├── file.go ├── file_test.go ├── health.go ├── health_test.go ├── logs.go ├── logs_test.go ├── middlewares.go ├── middlewares_test.go ├── realtime.go ├── realtime_test.go ├── record_auth.go ├── record_auth_test.go ├── record_crud.go ├── record_crud_test.go ├── record_helpers.go ├── record_helpers_test.go ├── serve.go ├── settings.go └── settings_test.go ├── cmd ├── admin.go ├── admin_test.go └── serve.go ├── core ├── app.go ├── base.go ├── base_backup.go ├── base_backup_test.go ├── base_settings_test.go ├── base_test.go ├── collections_cache.go ├── db_pg.go ├── events.go ├── events_test.go └── log_printer.go ├── daos ├── admin.go ├── admin_test.go ├── base.go ├── base_retry.go ├── base_retry_test.go ├── base_test.go ├── collection.go ├── collection_test.go ├── external_auth.go ├── external_auth_test.go ├── log.go ├── log_test.go ├── param.go ├── param_test.go ├── record.go ├── record_expand.go ├── record_expand_test.go ├── record_table_sync.go ├── record_table_sync_test.go ├── record_test.go ├── settings.go ├── settings_test.go ├── table.go ├── table_test.go ├── view.go └── view_test.go ├── docker-compose.yaml ├── drop.sh ├── examples └── base │ ├── .gitignore │ └── main.go ├── forms ├── admin_login.go ├── admin_login_test.go ├── admin_password_reset_confirm.go ├── admin_password_reset_confirm_test.go ├── admin_password_reset_request.go ├── admin_password_reset_request_test.go ├── admin_upsert.go ├── admin_upsert_test.go ├── apple_client_secret_create.go ├── apple_client_secret_create_test.go ├── backup_create.go ├── backup_create_test.go ├── backup_upload.go ├── backup_upload_test.go ├── base.go ├── collection_upsert.go ├── collection_upsert_test.go ├── collections_import.go ├── collections_import_test.go ├── realtime_subscribe.go ├── realtime_subscribe_test.go ├── record_email_change_confirm.go ├── record_email_change_confirm_test.go ├── record_email_change_request.go ├── record_email_change_request_test.go ├── record_oauth2_login.go ├── record_oauth2_login_test.go ├── record_password_login.go ├── record_password_login_test.go ├── record_password_reset_confirm.go ├── record_password_reset_confirm_test.go ├── record_password_reset_request.go ├── record_password_reset_request_test.go ├── record_upsert.go ├── record_upsert_test.go ├── record_verification_confirm.go ├── record_verification_confirm_test.go ├── record_verification_request.go ├── record_verification_request_test.go ├── settings_upsert.go ├── settings_upsert_test.go ├── test_email_send.go ├── test_email_send_test.go ├── test_s3_filesystem.go ├── test_s3_filesystem_test.go └── validators │ ├── file.go │ ├── file_test.go │ ├── model.go │ ├── model_test.go │ ├── record_data.go │ ├── record_data_test.go │ ├── string.go │ ├── string_test.go │ └── validators.go ├── go.mod ├── go.sum ├── golangci.yml ├── keys └── .gitignore ├── mails ├── admin.go ├── admin_test.go ├── base.go ├── record.go ├── record_test.go └── templates │ ├── admin_password_reset.go │ ├── html_content.go │ ├── layout.go │ └── password_login_alert.go ├── migrations ├── 1640988000_init.go ├── 1673167670_multi_match_migrate.go ├── 1677152688_rename_authentik_to_oidc.go ├── 1679943780_normalize_single_multiple_values.go ├── 1679943781_add_indexes_column.go ├── 1685164450_check_fk.go ├── 1689579878_renormalize_single_multiple_values.go ├── 1690319366_reset_null_values.go ├── 1690454337_transform_relations_to_views.go ├── 1691747913_resave_views.go ├── 1692609521_copy_display_fields.go ├── 1701496825_allow_single_oauth2_provider_in_multiple_auth_collections.go ├── 1702134272_set_default_json_max_size.go ├── 1718706525_add_login_alert_column.go └── logs │ ├── 1640988000_init.go │ ├── 1660821103_add_user_ip_column.go │ ├── 1677760279_uppsercase_method.go │ └── 1699187560_logs_generalization.go ├── models ├── admin.go ├── admin_test.go ├── backup_file_info.go ├── base.go ├── base_test.go ├── collection.go ├── collection_test.go ├── external_auth.go ├── external_auth_test.go ├── log.go ├── param.go ├── param_test.go ├── record.go ├── record_test.go ├── request.go ├── request_info.go ├── request_info_test.go ├── request_test.go ├── schema │ ├── schema.go │ ├── schema_field.go │ ├── schema_field_test.go │ └── schema_test.go ├── settings │ ├── settings.go │ ├── settings_templates.go │ └── settings_test.go └── table_info.go ├── plugins ├── ghupdate │ ├── ghupdate.go │ ├── ghupdate_test.go │ ├── release.go │ └── release_test.go ├── jsvm │ ├── binds.go │ ├── binds_test.go │ ├── form_data.go │ ├── form_data_test.go │ ├── internal │ │ └── types │ │ │ ├── generated │ │ │ ├── embed.go │ │ │ └── types.d.ts │ │ │ └── types.go │ ├── jsvm.go │ ├── mapper.go │ ├── mapper_test.go │ └── pool.go └── migratecmd │ ├── automigrate.go │ ├── migratecmd.go │ ├── migratecmd_test.go │ └── templates.go ├── resolvers ├── multi_match_subquery.go ├── record_field_resolve_runner.go ├── record_field_resolver.go ├── record_field_resolver_test.go └── resolvers.go ├── rocketbase.go ├── rocketbase_test.go ├── test_sh.sh ├── tests ├── api.go ├── app.go ├── auth_helper_test.go ├── data │ ├── .gitignore │ ├── empty.file │ └── storage │ │ ├── 9n89pl5vkct6330 │ │ ├── la4y2w4o98acwuj │ │ │ ├── 300_uh_lkx91_hvb_Da8K5pl069.png │ │ │ ├── 300_uh_lkx91_hvb_Da8K5pl069.png.attrs │ │ │ └── thumbs_300_uh_lkx91_hvb_Da8K5pl069.png │ │ │ │ ├── 100x100_300_uh_lkx91_hvb_Da8K5pl069.png │ │ │ │ └── 100x100_300_uh_lkx91_hvb_Da8K5pl069.png.attrs │ │ └── qjeql998mtp1azp │ │ │ ├── logo_vcf_jjg5_tah_9MtIHytOmZ.svg │ │ │ └── logo_vcf_jjg5_tah_9MtIHytOmZ.svg.attrs │ │ ├── _pb_users_auth_ │ │ ├── 4q1xlclmfloku33 │ │ │ ├── 300_1SEi6Q6U72.png │ │ │ ├── 300_1SEi6Q6U72.png.attrs │ │ │ └── thumbs_300_1SEi6Q6U72.png │ │ │ │ ├── 0x50_300_1SEi6Q6U72.png │ │ │ │ ├── 0x50_300_1SEi6Q6U72.png.attrs │ │ │ │ ├── 100x100_300_1SEi6Q6U72.png │ │ │ │ ├── 100x100_300_1SEi6Q6U72.png.attrs │ │ │ │ ├── 70x0_300_1SEi6Q6U72.png │ │ │ │ ├── 70x0_300_1SEi6Q6U72.png.attrs │ │ │ │ ├── 70x50_300_1SEi6Q6U72.png │ │ │ │ ├── 70x50_300_1SEi6Q6U72.png.attrs │ │ │ │ ├── 70x50b_300_1SEi6Q6U72.png │ │ │ │ ├── 70x50b_300_1SEi6Q6U72.png.attrs │ │ │ │ ├── 70x50f_300_1SEi6Q6U72.png │ │ │ │ ├── 70x50f_300_1SEi6Q6U72.png.attrs │ │ │ │ ├── 70x50t_300_1SEi6Q6U72.png │ │ │ │ └── 70x50t_300_1SEi6Q6U72.png.attrs │ │ └── oap640cot4yru2s │ │ │ ├── test_kfd2wYLxkz.txt │ │ │ └── test_kfd2wYLxkz.txt.attrs │ │ ├── wsmn24bux7wo113 │ │ ├── 84nmscqy84lsi1t │ │ │ ├── 300_WlbFWSGmW9.png │ │ │ ├── 300_WlbFWSGmW9.png.attrs │ │ │ ├── logo_vcfJJG5TAh.svg │ │ │ ├── logo_vcfJJG5TAh.svg.attrs │ │ │ ├── test_MaWC6mWyrP.txt │ │ │ ├── test_MaWC6mWyrP.txt.attrs │ │ │ ├── test_QZFjKjXchk.txt │ │ │ ├── test_QZFjKjXchk.txt.attrs │ │ │ ├── test_d61b33QdDU.txt │ │ │ ├── test_d61b33QdDU.txt.attrs │ │ │ ├── test_tC1Yc87DfC.txt │ │ │ ├── test_tC1Yc87DfC.txt.attrs │ │ │ └── thumbs_300_WlbFWSGmW9.png │ │ │ │ ├── 100x100_300_WlbFWSGmW9.png │ │ │ │ └── 100x100_300_WlbFWSGmW9.png.attrs │ │ └── al1h9ijdeojtsjy │ │ │ ├── 300_Jsjq7RdBgA.png │ │ │ ├── 300_Jsjq7RdBgA.png.attrs │ │ │ └── thumbs_300_Jsjq7RdBgA.png │ │ │ ├── 100x100_300_Jsjq7RdBgA.png │ │ │ └── 100x100_300_Jsjq7RdBgA.png.attrs │ │ └── wzlqyes4orhoygb │ │ ├── 7nwo8tuiatetxdm │ │ ├── test_JnXeKEwgwr.txt │ │ └── test_JnXeKEwgwr.txt.attrs │ │ ├── lcl9d87w22ml6jy │ │ ├── 300_UhLKX91HVb.png │ │ ├── 300_UhLKX91HVb.png.attrs │ │ ├── test_FLurQTgrY8.txt │ │ ├── test_FLurQTgrY8.txt.attrs │ │ └── thumbs_300_UhLKX91HVb.png │ │ │ ├── 100x100_300_UhLKX91HVb.png │ │ │ └── 100x100_300_UhLKX91HVb.png.attrs │ │ └── mk5fmymtx4wsprk │ │ ├── 300_JdfBOieXAW.png │ │ ├── 300_JdfBOieXAW.png.attrs │ │ └── thumbs_300_JdfBOieXAW.png │ │ ├── 100x100_300_JdfBOieXAW.png │ │ └── 100x100_300_JdfBOieXAW.png.attrs ├── logs.go ├── mailer.go └── request.go ├── tokens ├── admin.go ├── admin_test.go ├── record.go ├── record_test.go └── tokens.go ├── tools ├── archive │ ├── create.go │ ├── create_test.go │ ├── extract.go │ └── extract_test.go ├── auth │ ├── apple.go │ ├── auth.go │ ├── auth_test.go │ ├── base_provider.go │ ├── base_provider_test.go │ ├── bitbucket.go │ ├── discord.go │ ├── facebook.go │ ├── gitea.go │ ├── gitee.go │ ├── github.go │ ├── gitlab.go │ ├── google.go │ ├── instagram.go │ ├── kakao.go │ ├── livechat.go │ ├── mailcow.go │ ├── microsoft.go │ ├── oidc.go │ ├── patreon.go │ ├── planningcenter.go │ ├── spotify.go │ ├── strava.go │ ├── twitch.go │ ├── twitter.go │ ├── vk.go │ └── yandex.go ├── cron │ ├── cron.go │ ├── cron_test.go │ ├── schedule.go │ └── schedule_test.go ├── dbutils │ ├── index.go │ ├── index_test.go │ ├── json.go │ └── json_test.go ├── filesystem │ ├── file.go │ ├── file_test.go │ ├── filesystem.go │ ├── filesystem_test.go │ ├── ignore_signing_headers.go │ └── internal │ │ └── s3lite │ │ └── s3lite.go ├── hook │ ├── hook.go │ ├── hook_test.go │ ├── tagged.go │ └── tagged_test.go ├── inflector │ ├── inflector.go │ └── inflector_test.go ├── list │ ├── list.go │ └── list_test.go ├── logger │ ├── batch_handler.go │ ├── batch_handler_test.go │ └── log.go ├── mailer │ ├── html2text.go │ ├── html2text_test.go │ ├── mailer.go │ ├── sendmail.go │ ├── smtp.go │ └── smtp_test.go ├── migrate │ ├── list.go │ ├── list_test.go │ ├── runner.go │ └── runner_test.go ├── osutils │ ├── dir.go │ └── dir_test.go ├── rest │ ├── excerpt_modifier.go │ ├── excerpt_modifier_test.go │ ├── json_serializer.go │ ├── json_serializer_test.go │ ├── multi_binder.go │ ├── multi_binder_test.go │ ├── uploaded_file.go │ ├── uploaded_file_test.go │ ├── url.go │ └── url_test.go ├── routine │ ├── routine.go │ └── routine_test.go ├── search │ ├── filter.go │ ├── filter_test.go │ ├── identifier_macros.go │ ├── identifier_macros_test.go │ ├── provider.go │ ├── provider_test.go │ ├── simple_field_resolver.go │ ├── simple_field_resolver_test.go │ ├── sort.go │ └── sort_test.go ├── security │ ├── crypto.go │ ├── crypto_test.go │ ├── encrypt.go │ ├── encrypt_test.go │ ├── jwt.go │ ├── jwt_test.go │ ├── random.go │ └── random_test.go ├── store │ ├── store.go │ └── store_test.go ├── subscriptions │ ├── broker.go │ ├── broker_test.go │ ├── client.go │ └── client_test.go ├── template │ ├── registry.go │ ├── registry_test.go │ ├── renderer.go │ └── renderer_test.go ├── test_utils │ └── test_utils.go ├── tokenizer │ ├── tokenizer.go │ └── tokenizer_test.go └── types │ ├── datetime.go │ ├── datetime_test.go │ ├── json_array.go │ ├── json_array_test.go │ ├── json_map.go │ ├── json_map_test.go │ ├── json_raw.go │ ├── json_raw_test.go │ ├── types.go │ └── types_test.go ├── ui ├── .env ├── .env.development ├── .gitignore ├── README.md ├── dist │ ├── assets │ │ ├── AuthMethodsDocs-CrEPr1Fz.js │ │ ├── AuthRefreshDocs-1k07W-sL.js │ │ ├── AuthWithOAuth2Docs-6Svn1qsJ.js │ │ ├── AuthWithPasswordDocs-Deuu1l41.js │ │ ├── CodeEditor-CoTpL2eu.js │ │ ├── ConfirmEmailChangeDocs-DiV0Y4lP.js │ │ ├── ConfirmPasswordResetDocs-a6y5hSaH.js │ │ ├── ConfirmVerificationDocs-zC3h5AjK.js │ │ ├── CreateApiDocs-BhzDfmVz.js │ │ ├── DeleteApiDocs-DgdFM5Qa.js │ │ ├── FieldsQueryParam-BoYS20e2.js │ │ ├── FilterAutocompleteInput-C6aEn8iB.js │ │ ├── ListApiDocs-B8tjPuF-.js │ │ ├── ListApiDocs-ByASLUZu.css │ │ ├── ListExternalAuthsDocs-CeO88Sbf.js │ │ ├── PageAdminConfirmPasswordReset-Dp7faOXH.js │ │ ├── PageAdminRequestPasswordReset-MZKeemZH.js │ │ ├── PageOAuth2RedirectFailure-BS8Cmo1H.js │ │ ├── PageOAuth2RedirectSuccess-D1_hRUhp.js │ │ ├── PageRecordConfirmEmailChange-DnPlwx9F.js │ │ ├── PageRecordConfirmPasswordReset-Xbuf0XcM.js │ │ ├── PageRecordConfirmVerification-DA7_iPCH.js │ │ ├── RealtimeApiDocs-DIqCMSA9.js │ │ ├── RequestEmailChangeDocs-C-z_qGdi.js │ │ ├── RequestPasswordResetDocs-BrBiwzUj.js │ │ ├── RequestVerificationDocs-1QGGi1Cu.js │ │ ├── SdkTabs-C5m8rJlh.js │ │ ├── SdkTabs-CSWLpEgj.css │ │ ├── UnlinkExternalAuthDocs-DSSa7KWo.js │ │ ├── UpdateApiDocs-DV_6nyeC.js │ │ ├── ViewApiDocs-CaT6LoZ7.js │ │ ├── autocomplete.worker-CGnTjHzG.js │ │ ├── index-BH9k9ewW.js │ │ ├── index-CH1esvLs.js │ │ └── index-CyfS9YzQ.css │ ├── fonts │ │ ├── remixicon │ │ │ └── remixicon.woff2 │ │ ├── source-sans-pro │ │ │ ├── source-sans-pro-v18-latin_cyrillic-600.woff2 │ │ │ ├── source-sans-pro-v18-latin_cyrillic-600italic.woff2 │ │ │ ├── source-sans-pro-v18-latin_cyrillic-700.woff2 │ │ │ ├── source-sans-pro-v18-latin_cyrillic-700italic.woff2 │ │ │ ├── source-sans-pro-v18-latin_cyrillic-italic.woff2 │ │ │ └── source-sans-pro-v18-latin_cyrillic-regular.woff2 │ │ └── ubuntu-mono │ │ │ ├── ubuntu-mono-v17-cyrillic_latin-700.woff2 │ │ │ └── ubuntu-mono-v17-cyrillic_latin-regular.woff2 │ ├── images │ │ ├── avatars │ │ │ ├── avatar0.svg │ │ │ ├── avatar1.svg │ │ │ ├── avatar2.svg │ │ │ ├── avatar3.svg │ │ │ ├── avatar4.svg │ │ │ ├── avatar5.svg │ │ │ ├── avatar6.svg │ │ │ ├── avatar7.svg │ │ │ ├── avatar8.svg │ │ │ └── avatar9.svg │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── mstile-70x70.png │ │ │ ├── safari-pinned-tab.svg │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── oauth2 │ │ │ ├── apple.svg │ │ │ ├── bitbucket.svg │ │ │ ├── discord.svg │ │ │ ├── facebook.svg │ │ │ ├── gitea.svg │ │ │ ├── gitee.svg │ │ │ ├── github.svg │ │ │ ├── gitlab.svg │ │ │ ├── google.svg │ │ │ ├── instagram.svg │ │ │ ├── kakao.svg │ │ │ ├── livechat.svg │ │ │ ├── mailcow.svg │ │ │ ├── microsoft.svg │ │ │ ├── oidc.svg │ │ │ ├── patreon.svg │ │ │ ├── planningcenter.svg │ │ │ ├── spotify.svg │ │ │ ├── strava.svg │ │ │ ├── twitch.svg │ │ │ ├── twitter.svg │ │ │ ├── vk.svg │ │ │ └── yandex.svg │ ├── index.html │ └── libs │ │ ├── prism │ │ ├── prism.min.css │ │ └── prism.min.js │ │ └── tinymce │ │ ├── icons │ │ └── default │ │ │ └── icons.min.js │ │ ├── license.txt │ │ ├── models │ │ └── dom │ │ │ └── model.min.js │ │ ├── plugins │ │ ├── anchor │ │ │ └── plugin.min.js │ │ ├── autolink │ │ │ └── plugin.min.js │ │ ├── autoresize │ │ │ └── plugin.min.js │ │ ├── autosave │ │ │ └── plugin.min.js │ │ ├── code │ │ │ └── plugin.min.js │ │ ├── codesample │ │ │ └── plugin.min.js │ │ ├── directionality │ │ │ └── plugin.min.js │ │ ├── fullscreen │ │ │ └── plugin.min.js │ │ ├── image │ │ │ └── plugin.min.js │ │ ├── importcss │ │ │ └── plugin.min.js │ │ ├── insertdatetime │ │ │ └── plugin.min.js │ │ ├── link │ │ │ └── plugin.min.js │ │ ├── lists │ │ │ └── plugin.min.js │ │ ├── media │ │ │ └── plugin.min.js │ │ ├── nonbreaking │ │ │ └── plugin.min.js │ │ ├── pagebreak │ │ │ └── plugin.min.js │ │ ├── preview │ │ │ └── plugin.min.js │ │ ├── quickbars │ │ │ └── plugin.min.js │ │ ├── save │ │ │ └── plugin.min.js │ │ ├── searchreplace │ │ │ └── plugin.min.js │ │ ├── table │ │ │ └── plugin.min.js │ │ ├── template │ │ │ └── plugin.min.js │ │ ├── visualblocks │ │ │ └── plugin.min.js │ │ ├── visualchars │ │ │ └── plugin.min.js │ │ └── wordcount │ │ │ └── plugin.min.js │ │ ├── skins │ │ ├── content │ │ │ ├── dark │ │ │ │ └── content.min.css │ │ │ ├── default │ │ │ │ ├── content.js │ │ │ │ └── content.min.css │ │ │ ├── document │ │ │ │ └── content.min.css │ │ │ ├── pocketbase │ │ │ │ ├── content.css │ │ │ │ └── content.min.css │ │ │ └── writer │ │ │ │ └── content.min.css │ │ └── ui │ │ │ ├── oxide │ │ │ ├── content.inline.min.css │ │ │ ├── content.min.css │ │ │ ├── skin.min.css │ │ │ └── skin.shadowdom.min.css │ │ │ └── pocketbase │ │ │ ├── content.css │ │ │ ├── content.inline.css │ │ │ ├── content.inline.min.css │ │ │ ├── content.min.css │ │ │ ├── content.mobile.css │ │ │ ├── content.mobile.min.css │ │ │ ├── fonts │ │ │ └── tinymce-mobile.woff │ │ │ ├── skin.css │ │ │ ├── skin.min.css │ │ │ ├── skin.mobile.css │ │ │ └── skin.mobile.min.css │ │ ├── themes │ │ └── silver │ │ │ └── theme.min.js │ │ └── tinymce.min.js ├── embed.go ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public │ ├── fonts │ │ ├── remixicon │ │ │ └── remixicon.woff2 │ │ ├── source-sans-pro │ │ │ ├── source-sans-pro-v18-latin_cyrillic-600.woff2 │ │ │ ├── source-sans-pro-v18-latin_cyrillic-600italic.woff2 │ │ │ ├── source-sans-pro-v18-latin_cyrillic-700.woff2 │ │ │ ├── source-sans-pro-v18-latin_cyrillic-700italic.woff2 │ │ │ ├── source-sans-pro-v18-latin_cyrillic-italic.woff2 │ │ │ └── source-sans-pro-v18-latin_cyrillic-regular.woff2 │ │ └── ubuntu-mono │ │ │ ├── ubuntu-mono-v17-cyrillic_latin-700.woff2 │ │ │ └── ubuntu-mono-v17-cyrillic_latin-regular.woff2 │ ├── images │ │ ├── avatars │ │ │ ├── avatar0.svg │ │ │ ├── avatar1.svg │ │ │ ├── avatar2.svg │ │ │ ├── avatar3.svg │ │ │ ├── avatar4.svg │ │ │ ├── avatar5.svg │ │ │ ├── avatar6.svg │ │ │ ├── avatar7.svg │ │ │ ├── avatar8.svg │ │ │ └── avatar9.svg │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── mstile-70x70.png │ │ │ ├── safari-pinned-tab.svg │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── oauth2 │ │ │ ├── apple.svg │ │ │ ├── bitbucket.svg │ │ │ ├── discord.svg │ │ │ ├── facebook.svg │ │ │ ├── gitea.svg │ │ │ ├── gitee.svg │ │ │ ├── github.svg │ │ │ ├── gitlab.svg │ │ │ ├── google.svg │ │ │ ├── instagram.svg │ │ │ ├── kakao.svg │ │ │ ├── livechat.svg │ │ │ ├── mailcow.svg │ │ │ ├── microsoft.svg │ │ │ ├── oidc.svg │ │ │ ├── patreon.svg │ │ │ ├── planningcenter.svg │ │ │ ├── spotify.svg │ │ │ ├── strava.svg │ │ │ ├── twitch.svg │ │ │ ├── twitter.svg │ │ │ ├── vk.svg │ │ │ └── yandex.svg │ └── libs │ │ ├── prism │ │ ├── prism.min.css │ │ └── prism.min.js │ │ └── tinymce │ │ ├── icons │ │ └── default │ │ │ └── icons.min.js │ │ ├── license.txt │ │ ├── models │ │ └── dom │ │ │ └── model.min.js │ │ ├── plugins │ │ ├── anchor │ │ │ └── plugin.min.js │ │ ├── autolink │ │ │ └── plugin.min.js │ │ ├── autoresize │ │ │ └── plugin.min.js │ │ ├── autosave │ │ │ └── plugin.min.js │ │ ├── code │ │ │ └── plugin.min.js │ │ ├── codesample │ │ │ └── plugin.min.js │ │ ├── directionality │ │ │ └── plugin.min.js │ │ ├── fullscreen │ │ │ └── plugin.min.js │ │ ├── image │ │ │ └── plugin.min.js │ │ ├── importcss │ │ │ └── plugin.min.js │ │ ├── insertdatetime │ │ │ └── plugin.min.js │ │ ├── link │ │ │ └── plugin.min.js │ │ ├── lists │ │ │ └── plugin.min.js │ │ ├── media │ │ │ └── plugin.min.js │ │ ├── nonbreaking │ │ │ └── plugin.min.js │ │ ├── pagebreak │ │ │ └── plugin.min.js │ │ ├── preview │ │ │ └── plugin.min.js │ │ ├── quickbars │ │ │ └── plugin.min.js │ │ ├── save │ │ │ └── plugin.min.js │ │ ├── searchreplace │ │ │ └── plugin.min.js │ │ ├── table │ │ │ └── plugin.min.js │ │ ├── template │ │ │ └── plugin.min.js │ │ ├── visualblocks │ │ │ └── plugin.min.js │ │ ├── visualchars │ │ │ └── plugin.min.js │ │ └── wordcount │ │ │ └── plugin.min.js │ │ ├── skins │ │ ├── content │ │ │ ├── dark │ │ │ │ └── content.min.css │ │ │ ├── default │ │ │ │ ├── content.js │ │ │ │ └── content.min.css │ │ │ ├── document │ │ │ │ └── content.min.css │ │ │ ├── pocketbase │ │ │ │ ├── content.css │ │ │ │ └── content.min.css │ │ │ └── writer │ │ │ │ └── content.min.css │ │ └── ui │ │ │ ├── oxide │ │ │ ├── content.inline.min.css │ │ │ ├── content.min.css │ │ │ ├── skin.min.css │ │ │ └── skin.shadowdom.min.css │ │ │ └── pocketbase │ │ │ ├── content.css │ │ │ ├── content.inline.css │ │ │ ├── content.inline.min.css │ │ │ ├── content.min.css │ │ │ ├── content.mobile.css │ │ │ ├── content.mobile.min.css │ │ │ ├── fonts │ │ │ └── tinymce-mobile.woff │ │ │ ├── skin.css │ │ │ ├── skin.min.css │ │ │ ├── skin.mobile.css │ │ │ └── skin.mobile.min.css │ │ ├── themes │ │ └── silver │ │ │ └── theme.min.js │ │ └── tinymce.min.js ├── src │ ├── App.svelte │ ├── actions │ │ ├── scrollend.js │ │ └── tooltip.js │ ├── autocomplete.worker.js │ ├── components │ │ ├── PageIndex.svelte │ │ ├── admins │ │ │ ├── AdminUpsertPanel.svelte │ │ │ ├── PageAdminConfirmPasswordReset.svelte │ │ │ ├── PageAdminLogin.svelte │ │ │ ├── PageAdminRequestPasswordReset.svelte │ │ │ └── PageAdmins.svelte │ │ ├── base │ │ │ ├── Accordion.svelte │ │ │ ├── AutoExpandTextarea.svelte │ │ │ ├── BaseSelectOption.svelte │ │ │ ├── CodeBlock.svelte │ │ │ ├── CodeEditor.svelte │ │ │ ├── Confirmation.svelte │ │ │ ├── CopyIcon.svelte │ │ │ ├── Draggable.svelte │ │ │ ├── Dragline.svelte │ │ │ ├── Field.svelte │ │ │ ├── FilterAutocompleteInput.svelte │ │ │ ├── FormattedDate.svelte │ │ │ ├── FullPage.svelte │ │ │ ├── InitialsAvatar.svelte │ │ │ ├── Installer.svelte │ │ │ ├── MimeTypeSelectOption.svelte │ │ │ ├── ModelDateIcon.svelte │ │ │ ├── MultipleValueInput.svelte │ │ │ ├── ObjectSelect.svelte │ │ │ ├── OverlayPanel.svelte │ │ │ ├── PageSidebar.svelte │ │ │ ├── PageWrapper.svelte │ │ │ ├── PreviewPopup.svelte │ │ │ ├── RedactedPasswordInput.svelte │ │ │ ├── RefreshButton.svelte │ │ │ ├── Scroller.svelte │ │ │ ├── Searchbar.svelte │ │ │ ├── SecretGeneratorButton.svelte │ │ │ ├── Select.svelte │ │ │ ├── SortHeader.svelte │ │ │ ├── TinyMCE.svelte │ │ │ ├── Toasts.svelte │ │ │ ├── Toggler.svelte │ │ │ └── UploadedFilePreview.svelte │ │ ├── collections │ │ │ ├── CollectionAuthOptionsTab.svelte │ │ │ ├── CollectionDocsPanel.svelte │ │ │ ├── CollectionFieldsTab.svelte │ │ │ ├── CollectionQueryTab.svelte │ │ │ ├── CollectionRulesTab.svelte │ │ │ ├── CollectionSidebarItem.svelte │ │ │ ├── CollectionUpdateConfirm.svelte │ │ │ ├── CollectionUpsertPanel.svelte │ │ │ ├── CollectionsDiffTable.svelte │ │ │ ├── CollectionsSidebar.svelte │ │ │ ├── IndexUpsertPanel.svelte │ │ │ ├── IndexesList.svelte │ │ │ ├── RuleField.svelte │ │ │ ├── docs │ │ │ │ ├── AuthMethodsDocs.svelte │ │ │ │ ├── AuthRefreshDocs.svelte │ │ │ │ ├── AuthWithOAuth2Docs.svelte │ │ │ │ ├── AuthWithPasswordDocs.svelte │ │ │ │ ├── ConfirmEmailChangeDocs.svelte │ │ │ │ ├── ConfirmPasswordResetDocs.svelte │ │ │ │ ├── ConfirmVerificationDocs.svelte │ │ │ │ ├── CreateApiDocs.svelte │ │ │ │ ├── DeleteApiDocs.svelte │ │ │ │ ├── FieldsQueryParam.svelte │ │ │ │ ├── FilterSyntax.svelte │ │ │ │ ├── ListApiDocs.svelte │ │ │ │ ├── ListExternalAuthsDocs.svelte │ │ │ │ ├── RealtimeApiDocs.svelte │ │ │ │ ├── RequestEmailChangeDocs.svelte │ │ │ │ ├── RequestPasswordResetDocs.svelte │ │ │ │ ├── RequestVerificationDocs.svelte │ │ │ │ ├── SdkTabs.svelte │ │ │ │ ├── UnlinkExternalAuthDocs.svelte │ │ │ │ ├── UpdateApiDocs.svelte │ │ │ │ └── ViewApiDocs.svelte │ │ │ └── schema │ │ │ │ ├── NewField.svelte │ │ │ │ ├── SchemaField.svelte │ │ │ │ ├── SchemaFieldBool.svelte │ │ │ │ ├── SchemaFieldDate.svelte │ │ │ │ ├── SchemaFieldEditor.svelte │ │ │ │ ├── SchemaFieldEmail.svelte │ │ │ │ ├── SchemaFieldFile.svelte │ │ │ │ ├── SchemaFieldJson.svelte │ │ │ │ ├── SchemaFieldNumber.svelte │ │ │ │ ├── SchemaFieldRelation.svelte │ │ │ │ ├── SchemaFieldSelect.svelte │ │ │ │ ├── SchemaFieldText.svelte │ │ │ │ └── SchemaFieldUrl.svelte │ │ ├── logs │ │ │ ├── LogDate.svelte │ │ │ ├── LogLevel.svelte │ │ │ ├── LogViewPanel.svelte │ │ │ ├── LogsChart.svelte │ │ │ ├── LogsLevelsInfo.svelte │ │ │ ├── LogsList.svelte │ │ │ ├── LogsSettingsPanel.svelte │ │ │ └── PageLogs.svelte │ │ ├── records │ │ │ ├── ExternalAuthsList.svelte │ │ │ ├── PageOAuth2Redirect.svelte │ │ │ ├── PageOAuth2RedirectFailure.svelte │ │ │ ├── PageOAuth2RedirectSuccess.svelte │ │ │ ├── PageRecordConfirmEmailChange.svelte │ │ │ ├── PageRecordConfirmPasswordReset.svelte │ │ │ ├── PageRecordConfirmVerification.svelte │ │ │ ├── PageRecords.svelte │ │ │ ├── RecordFieldValue.svelte │ │ │ ├── RecordFilePicker.svelte │ │ │ ├── RecordFileThumb.svelte │ │ │ ├── RecordInfo.svelte │ │ │ ├── RecordPreviewPanel.svelte │ │ │ ├── RecordUpsertPanel.svelte │ │ │ ├── RecordsCount.svelte │ │ │ ├── RecordsList.svelte │ │ │ ├── RecordsPicker.svelte │ │ │ └── fields │ │ │ │ ├── AuthFields.svelte │ │ │ │ ├── BoolField.svelte │ │ │ │ ├── DateField.svelte │ │ │ │ ├── EditorField.svelte │ │ │ │ ├── EmailField.svelte │ │ │ │ ├── FileField.svelte │ │ │ │ ├── JsonField.svelte │ │ │ │ ├── NumberField.svelte │ │ │ │ ├── RelationField.svelte │ │ │ │ ├── SelectField.svelte │ │ │ │ ├── TextField.svelte │ │ │ │ └── UrlField.svelte │ │ └── settings │ │ │ ├── AuthProviderCard.svelte │ │ │ ├── AuthProviderPanel.svelte │ │ │ ├── BackupCreatePanel.svelte │ │ │ ├── BackupRestorePanel.svelte │ │ │ ├── BackupUploadButton.svelte │ │ │ ├── BackupsList.svelte │ │ │ ├── EmailTemplateAccordion.svelte │ │ │ ├── EmailTestPopup.svelte │ │ │ ├── ImportPopup.svelte │ │ │ ├── PageApplication.svelte │ │ │ ├── PageAuthProviders.svelte │ │ │ ├── PageBackups.svelte │ │ │ ├── PageExportCollections.svelte │ │ │ ├── PageImportCollections.svelte │ │ │ ├── PageMail.svelte │ │ │ ├── PageStorage.svelte │ │ │ ├── PageTokenOptions.svelte │ │ │ ├── S3Fields.svelte │ │ │ ├── SettingsSidebar.svelte │ │ │ ├── TokenField.svelte │ │ │ └── providers │ │ │ ├── AppleOptions.svelte │ │ │ ├── AppleSecretPopup.svelte │ │ │ ├── MicrosoftOptions.svelte │ │ │ ├── OIDCOptions.svelte │ │ │ └── SelfHostedOptions.svelte │ ├── main.js │ ├── mimes.js │ ├── providers.js │ ├── routes.js │ ├── scss │ │ ├── _accordion.scss │ │ ├── _alert.scss │ │ ├── _animations.scss │ │ ├── _base.scss │ │ ├── _bulkbar.scss │ │ ├── _collections_export.scss │ │ ├── _docs_panel.scss │ │ ├── _dropdown.scss │ │ ├── _file_picker.scss │ │ ├── _flatpickr.scss │ │ ├── _fonts.scss │ │ ├── _form.scss │ │ ├── _grid.scss │ │ ├── _icons.scss │ │ ├── _layout.scss │ │ ├── _mixins.scss │ │ ├── _overlay_panel.scss │ │ ├── _reset.scss │ │ ├── _schema_field.scss │ │ ├── _searchbar.scss │ │ ├── _table.scss │ │ ├── _tabs.scss │ │ ├── _tooltip.scss │ │ ├── _vars.scss │ │ ├── main.scss │ │ └── prism_light.scss │ ├── stores │ │ ├── admin.js │ │ ├── app.js │ │ ├── collections.js │ │ ├── confirmation.js │ │ ├── errors.js │ │ └── toasts.js │ └── utils │ │ ├── ApiClient.js │ │ └── CommonHelper.js └── vite.config.js └── volumes └── .gitignore /.dockerignore: -------------------------------------------------------------------------------- 1 | /.vscode/ 2 | .idea 3 | 4 | .DS_Store 5 | 6 | # goreleaser builds folder 7 | /.builds/ 8 | 9 | # tests coverage 10 | coverage.out 11 | 12 | # plaintask todo files 13 | *.todo 14 | 15 | # generated markdown previews 16 | README.html 17 | CHANGELOG.html 18 | LICENSE.html 19 | 20 | # application specific ignores and go build outputs 21 | volumes/* 22 | pb_migrations/* 23 | pb_data/* 24 | server 25 | *.exe 26 | keys/* -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | LOGS_DATABASE="postgresql://your_user:your_pass@localhost/rocketbase_logs?sslmode=disable" 2 | DATABASE="postgresql://your_user:your_pass@localhost/rocketbase?sslmode=disable" 3 | BCRYPT_COST=10 # default is 12 4 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | LOGS_DATABASE="postgresql://your_user:your_pass@localhost/test_rocketbase_logs?sslmode=disable" 2 | DATABASE="postgresql://your_user:your_pass@localhost/test_rocketbase?sslmode=disable" 3 | BCRYPT_COST=10 # default is 12 4 | -------------------------------------------------------------------------------- /.env.test.basic: -------------------------------------------------------------------------------- 1 | LOGS_DATABASE="postgresql://your_user:your_pass@localhost/test_rocketbase_logs_basic?sslmode=disable" 2 | DATABASE="postgresql://your_user:your_pass@localhost/test_rocketbase_basic?sslmode=disable" 3 | BCRYPT_COST=10 # default is 12 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode/ 2 | .idea 3 | 4 | .DS_Store 5 | 6 | # goreleaser builds folder 7 | /.builds/ 8 | 9 | # tests coverage 10 | coverage.out 11 | 12 | # plaintask todo files 13 | *.todo 14 | 15 | # generated markdown previews 16 | README.html 17 | CHANGELOG.html 18 | LICENSE.html 19 | 20 | # application specific ignores and go build outputs 21 | volumes/* 22 | pb_migrations/* 23 | pb_data/* 24 | server 25 | *.exe 26 | keys/* 27 | .env 28 | tmp/ -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: pocketbase 2 | 3 | dist: .builds 4 | 5 | before: 6 | hooks: 7 | - go mod tidy 8 | 9 | builds: 10 | # used only for tests 11 | - id: build_cgo 12 | main: ./examples/base 13 | binary: pocketbase 14 | ldflags: 15 | - -s -w -X github.com/pocketbase/pocketbase.Version={{ .Version }} 16 | env: 17 | - CGO_ENABLED=1 18 | goos: 19 | - linux 20 | goarch: 21 | - amd64 22 | tags: 23 | - pq 24 | - sqlite 25 | 26 | - id: build_noncgo 27 | main: ./examples/base 28 | binary: pocketbase 29 | ldflags: 30 | - -s -w -X github.com/pocketbase/pocketbase.Version={{ .Version }} 31 | env: 32 | - CGO_ENABLED=0 33 | goos: 34 | - linux 35 | - windows 36 | - darwin 37 | goarch: 38 | - amd64 39 | - arm64 40 | - arm 41 | tags: 42 | - pq 43 | goarm: 44 | - 7 45 | ignore: 46 | - goos: windows 47 | goarch: arm 48 | - goos: darwin 49 | goarch: arm 50 | 51 | release: 52 | draft: true 53 | 54 | archives: 55 | - id: archive_noncgo 56 | builds: [build_noncgo] 57 | format: zip 58 | files: 59 | - LICENSE.md 60 | - CHANGELOG.md 61 | 62 | checksum: 63 | name_template: 'checksums.txt' 64 | 65 | snapshot: 66 | name_template: '{{ incpatch .Version }}-next' 67 | 68 | changelog: 69 | sort: asc 70 | filters: 71 | exclude: 72 | - '^examples:' 73 | - '^ui:' 74 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.21.0 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to RocketBase 2 | 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.21.5 2 | ARG ALPINE_VERSION=3.17 3 | 4 | # build stage 5 | FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-env 6 | RUN apk add build-base 7 | 8 | ADD . /src 9 | 10 | # CGO_ENABLED=0 11 | ENV CGO_ENABLED=0 12 | RUN cd /src && go build -tags pq -o server ./examples/base/main.go 13 | 14 | # final stage 15 | FROM alpine:${ALPINE_VERSION} 16 | 17 | RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* 18 | WORKDIR /app 19 | 20 | COPY --from=build-env /src/server /app/ 21 | 22 | # create user and give it permissions 23 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup 24 | RUN mkdir /app/pb_data 25 | RUN mkdir /app/pb_migrations 26 | RUN mkdir /app/pb_config 27 | 28 | RUN chown -R appuser:appgroup /app 29 | USER appuser 30 | 31 | CMD ["/app/server","serve", "--http=0.0.0.0:8090"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2022 - present, Gani Georgiev 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 5 | and associated documentation files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 7 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or 11 | substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 14 | BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 16 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | golangci-lint run -c ./golangci.yml ./... 3 | 4 | test: 5 | go test ./... -v --cover 6 | 7 | test-fast: 8 | go test -failfast -v ./... 9 | 10 | jstypes: 11 | go run ./plugins/jsvm/internal/types/types.go 12 | 13 | test-report: 14 | go test ./... -v --cover -coverprofile=coverage.out 15 | go tool cover -html=coverage.out 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Rocketbase 2 | 3 | > Rocketbase = Pocketbase + PostgreSQL + a lot of goodies 4 | 5 | ## Setup 6 | 7 | ```bash 8 | psql 9 | ``` 10 | 11 | ```sql 12 | CREATE 13 | USER your_user WITH PASSWORD 'your_pass'; 14 | CREATE 15 | DATABASE rocketbase; 16 | CREATE 17 | DATABASE rocketbase_logs; 18 | GRANT ALL PRIVILEGES ON DATABASE 19 | rocketbase TO your_user; 20 | GRANT ALL PRIVILEGES ON DATABASE 21 | rocketbase_logs TO your_user; 22 | -- test 23 | CREATE 24 | DATABASE test_rocketbase; 25 | CREATE 26 | DATABASE test_rocketbase_logs; 27 | GRANT ALL PRIVILEGES ON DATABASE 28 | test_rocketbase TO your_user; 29 | GRANT ALL PRIVILEGES ON DATABASE 30 | test_rocketbase_logs TO your_user; 31 | 32 | CREATE 33 | DATABASE test_rocketbase_basic; 34 | CREATE 35 | DATABASE test_rocketbase_logs_basic; 36 | GRANT ALL PRIVILEGES ON DATABASE 37 | test_rocketbase_basic TO your_user; 38 | GRANT ALL PRIVILEGES ON DATABASE 39 | test_rocketbase_logs_basic TO your_user; 40 | ``` 41 | 42 | ## Credit 43 | 44 | - pocketbase for main codebase 45 | - postgresbase for adapting postgres 46 | 47 | -------------------------------------------------------------------------------- /apis/health.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/hylarucoder/rocketbase/core" 7 | "github.com/labstack/echo/v5" 8 | ) 9 | 10 | // bindHealthApi registers the health api endpoint. 11 | func bindHealthApi(app core.App, rg *echo.Group) { 12 | api := healthApi{app: app} 13 | 14 | subGroup := rg.Group("/health") 15 | subGroup.HEAD("", api.healthCheck) 16 | subGroup.GET("", api.healthCheck) 17 | } 18 | 19 | type healthApi struct { 20 | app core.App 21 | } 22 | 23 | type healthCheckResponse struct { 24 | Message string `json:"message"` 25 | Code int `json:"code"` 26 | Data struct { 27 | CanBackup bool `json:"canBackup"` 28 | } `json:"data"` 29 | } 30 | 31 | // healthCheck returns a 200 OK response if the server is healthy. 32 | func (api *healthApi) healthCheck(c echo.Context) error { 33 | if c.Request().Method == http.MethodHead { 34 | return c.NoContent(http.StatusOK) 35 | } 36 | 37 | resp := new(healthCheckResponse) 38 | resp.Code = http.StatusOK 39 | resp.Message = "API is healthy." 40 | resp.Data.CanBackup = !api.app.Store().Has(core.StoreKeyActiveBackup) 41 | 42 | return c.JSON(http.StatusOK, resp) 43 | } 44 | -------------------------------------------------------------------------------- /apis/health_test.go: -------------------------------------------------------------------------------- 1 | package apis_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/hylarucoder/rocketbase/tests" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | func (suite *HealthTestSuite) TestHealthAPI() { 12 | t := suite.T() 13 | 14 | scenarios := []tests.ApiScenario{ 15 | { 16 | Name: "HEAD health status", 17 | Method: http.MethodHead, 18 | Url: "/api/health", 19 | ExpectedStatus: 200, 20 | }, 21 | { 22 | Name: "GET health status", 23 | Method: http.MethodGet, 24 | Url: "/api/health", 25 | ExpectedStatus: 200, 26 | ExpectedContent: []string{ 27 | `"code":200`, 28 | `"data":{`, 29 | `"canBackup":true`, 30 | }, 31 | }, 32 | } 33 | 34 | for _, scenario := range scenarios { 35 | scenario.Test(t) 36 | } 37 | } 38 | 39 | type HealthTestSuite struct { 40 | suite.Suite 41 | App *tests.TestApp 42 | Var int 43 | } 44 | 45 | func (suite *HealthTestSuite) SetupSuite() { 46 | app, _ := tests.NewTestApp() 47 | suite.Var = 5 48 | suite.App = app 49 | } 50 | 51 | func (suite *HealthTestSuite) TearDownSuite() { 52 | suite.App.Cleanup() 53 | } 54 | 55 | func TestHealthTestSuite(t *testing.T) { 56 | suite.Run(t, new(HealthTestSuite)) 57 | } 58 | -------------------------------------------------------------------------------- /core/db_pg.go: -------------------------------------------------------------------------------- 1 | //TODO: go:build pq 2 | 3 | package core 4 | 5 | import ( 6 | _ "github.com/lib/pq" 7 | "github.com/pocketbase/dbx" 8 | ) 9 | 10 | func ConnectDB(dsn string) (*dbx.DB, error) { 11 | return dbx.MustOpen("postgres", dsn) 12 | } 13 | -------------------------------------------------------------------------------- /examples/base/.gitignore: -------------------------------------------------------------------------------- 1 | # ignore everything 2 | /* 3 | 4 | # exclude from the ignore filter 5 | !.gitignore 6 | !main.go 7 | -------------------------------------------------------------------------------- /forms/base.go: -------------------------------------------------------------------------------- 1 | // Package models implements various services used for request data 2 | // validation and applying changes to existing DB models through the app Dao. 3 | package forms 4 | 5 | import ( 6 | "regexp" 7 | ) 8 | 9 | // base ID value regex pattern 10 | var idRegex = regexp.MustCompile(`^[^\@\#\$\&\|\.\,\'\"\\\/\s]+$`) 11 | 12 | // InterceptorNextFunc is a interceptor handler function. 13 | // Usually used in combination with InterceptorFunc. 14 | type InterceptorNextFunc[T any] func(t T) error 15 | 16 | // InterceptorFunc defines a single interceptor function that 17 | // will execute the provided next func handler. 18 | type InterceptorFunc[T any] func(next InterceptorNextFunc[T]) InterceptorNextFunc[T] 19 | 20 | // runInterceptors executes the provided list of interceptors. 21 | func runInterceptors[T any]( 22 | data T, 23 | next InterceptorNextFunc[T], 24 | interceptors ...InterceptorFunc[T], 25 | ) error { 26 | for i := len(interceptors) - 1; i >= 0; i-- { 27 | next = interceptors[i](next) 28 | } 29 | 30 | return next(data) 31 | } 32 | -------------------------------------------------------------------------------- /forms/realtime_subscribe.go: -------------------------------------------------------------------------------- 1 | package forms 2 | 3 | import ( 4 | validation "github.com/go-ozzo/ozzo-validation/v4" 5 | ) 6 | 7 | // RealtimeSubscribe is a realtime subscriptions request form. 8 | type RealtimeSubscribe struct { 9 | ClientId string `form:"clientId" json:"clientId"` 10 | Subscriptions []string `form:"subscriptions" json:"subscriptions"` 11 | } 12 | 13 | // NewRealtimeSubscribe creates new RealtimeSubscribe request form. 14 | func NewRealtimeSubscribe() *RealtimeSubscribe { 15 | return &RealtimeSubscribe{} 16 | } 17 | 18 | // Validate makes the form validatable by implementing [validation.Validatable] interface. 19 | func (form *RealtimeSubscribe) Validate() error { 20 | return validation.ValidateStruct(form, 21 | validation.Field(&form.ClientId, validation.Required, validation.Length(1, 255)), 22 | validation.Field(&form.Subscriptions, validation.Length(0, 1000)), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /forms/realtime_subscribe_test.go: -------------------------------------------------------------------------------- 1 | package forms_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/hylarucoder/rocketbase/forms" 8 | ) 9 | 10 | func TestRealtimeSubscribeValidate(t *testing.T) { 11 | t.Parallel() 12 | 13 | scenarios := []struct { 14 | clientId string 15 | expectError bool 16 | }{ 17 | {"", true}, 18 | {strings.Repeat("a", 256), true}, 19 | {"test", false}, 20 | } 21 | 22 | for i, s := range scenarios { 23 | form := forms.NewRealtimeSubscribe() 24 | form.ClientId = s.clientId 25 | 26 | err := form.Validate() 27 | 28 | hasErr := err != nil 29 | if hasErr != s.expectError { 30 | t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /forms/validators/model.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | 7 | validation "github.com/go-ozzo/ozzo-validation/v4" 8 | "github.com/hylarucoder/rocketbase/daos" 9 | "github.com/pocketbase/dbx" 10 | ) 11 | 12 | // UniqueId checks whether the provided model id already exists. 13 | // 14 | // Example: 15 | // 16 | // validation.Field(&form.Id, validation.By(validators.UniqueId(form.dao, tableName))) 17 | func UniqueId(dao *daos.Dao, tableName string) validation.RuleFunc { 18 | return func(value any) error { 19 | v, _ := value.(string) 20 | if v == "" { 21 | return nil // nothing to check 22 | } 23 | 24 | var foundId string 25 | 26 | err := dao.DB(). 27 | Select("id"). 28 | From(tableName). 29 | Where(dbx.HashExp{"id": v}). 30 | Limit(1). 31 | Row(&foundId) 32 | 33 | if (err != nil && !errors.Is(err, sql.ErrNoRows)) || foundId != "" { 34 | return validation.NewError("validation_invalid_id", "The model id is invalid or already exists.") 35 | } 36 | 37 | return nil 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /forms/validators/model_test.go: -------------------------------------------------------------------------------- 1 | package validators_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hylarucoder/rocketbase/forms/validators" 7 | "github.com/hylarucoder/rocketbase/tests" 8 | ) 9 | 10 | func TestUniqueId(t *testing.T) { 11 | t.Parallel() 12 | 13 | app, _ := tests.NewTestApp() 14 | defer app.Cleanup() 15 | 16 | scenarios := []struct { 17 | id string 18 | tableName string 19 | expectError bool 20 | }{ 21 | {"", "", false}, 22 | {"test", "", true}, 23 | {"2108348993330216960", "_collections", true}, 24 | {"test_unique_id", "unknown_table", true}, 25 | {"test_unique_id", "_collections", false}, 26 | } 27 | 28 | for i, s := range scenarios { 29 | err := validators.UniqueId(app.Dao(), s.tableName)(s.id) 30 | 31 | hasErr := err != nil 32 | if hasErr != s.expectError { 33 | t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /forms/validators/string.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | validation "github.com/go-ozzo/ozzo-validation/v4" 5 | ) 6 | 7 | // Compare checks whether the validated value matches another string. 8 | // 9 | // Example: 10 | // 11 | // validation.Field(&form.PasswordConfirm, validation.By(validators.Compare(form.Password))) 12 | func Compare(valueToCompare string) validation.RuleFunc { 13 | return func(value any) error { 14 | v, _ := value.(string) 15 | 16 | if v != valueToCompare { 17 | return validation.NewError("validation_values_mismatch", "Values don't match.") 18 | } 19 | 20 | return nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /forms/validators/string_test.go: -------------------------------------------------------------------------------- 1 | package validators_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hylarucoder/rocketbase/forms/validators" 7 | ) 8 | 9 | func TestCompare(t *testing.T) { 10 | t.Parallel() 11 | 12 | scenarios := []struct { 13 | valA string 14 | valB string 15 | expectError bool 16 | }{ 17 | {"", "", false}, 18 | {"", "456", true}, 19 | {"123", "", true}, 20 | {"123", "456", true}, 21 | {"123", "123", false}, 22 | } 23 | 24 | for i, s := range scenarios { 25 | err := validators.Compare(s.valA)(s.valB) 26 | 27 | hasErr := err != nil 28 | if hasErr != s.expectError { 29 | t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /forms/validators/validators.go: -------------------------------------------------------------------------------- 1 | // Package validators implements custom shared PocketBase validators. 2 | package validators 3 | -------------------------------------------------------------------------------- /golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | go: 1.21 3 | concurrency: 4 4 | timeout: 10m 5 | 6 | linters: 7 | disable-all: true 8 | enable: 9 | - asciicheck 10 | - gofmt 11 | - goimports 12 | - gomodguard 13 | - goprintffuncname 14 | - gosimple 15 | - govet 16 | - ineffassign 17 | - misspell 18 | - nakedret 19 | - nolintlint 20 | - prealloc 21 | - staticcheck 22 | - typecheck 23 | - unconvert 24 | - unused 25 | - whitespace -------------------------------------------------------------------------------- /keys/.gitignore: -------------------------------------------------------------------------------- 1 | ./* 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /mails/admin_test.go: -------------------------------------------------------------------------------- 1 | package mails_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/hylarucoder/rocketbase/mails" 8 | "github.com/hylarucoder/rocketbase/tests" 9 | ) 10 | 11 | func TestSendAdminPasswordReset(t *testing.T) { 12 | t.Parallel() 13 | 14 | testApp, _ := tests.NewTestApp() 15 | defer testApp.Cleanup() 16 | 17 | // ensure that action url normalization will be applied 18 | testApp.Settings().Meta.AppUrl = "http://localhost:8090////" 19 | 20 | admin, _ := testApp.Dao().FindAdminByEmail("test@example.com") 21 | 22 | err := mails.SendAdminPasswordReset(testApp, admin) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | if testApp.TestMailer.TotalSend != 1 { 28 | t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend) 29 | } 30 | 31 | expectedParts := []string{ 32 | "http://localhost:8090/_/#/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", 33 | } 34 | for _, part := range expectedParts { 35 | if !strings.Contains(testApp.TestMailer.LastMessage.HTML, part) { 36 | t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastMessage.HTML) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /mails/base.go: -------------------------------------------------------------------------------- 1 | // Package mails implements various helper methods for sending user and admin 2 | // emails like forgotten password, verification, etc. 3 | package mails 4 | 5 | import ( 6 | "bytes" 7 | "text/template" 8 | ) 9 | 10 | // resolveTemplateContent resolves inline html template strings. 11 | func resolveTemplateContent(data any, content ...string) (string, error) { 12 | if len(content) == 0 { 13 | return "", nil 14 | } 15 | 16 | t := template.New("inline_template") 17 | 18 | var parseErr error 19 | for _, v := range content { 20 | t, parseErr = t.Parse(v) 21 | if parseErr != nil { 22 | return "", parseErr 23 | } 24 | } 25 | 26 | var wr bytes.Buffer 27 | 28 | if executeErr := t.Execute(&wr, data); executeErr != nil { 29 | return "", executeErr 30 | } 31 | 32 | return wr.String(), nil 33 | } 34 | -------------------------------------------------------------------------------- /mails/templates/admin_password_reset.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | // Available variables: 4 | // 5 | // ``` 6 | // Admin *models.Admin 7 | // AppName string 8 | // AppUrl string 9 | // Token string 10 | // ActionUrl string 11 | // ``` 12 | const AdminPasswordResetBody = ` 13 | {{define "content"}} 14 |

Hello,

15 |

Follow this link to reset your admin password for {{.AppName}}.

16 |

17 | Reset password 18 |

19 |

If you did not request to reset your password, please ignore this email and the link will expire on its own.

20 | {{end}} 21 | ` 22 | -------------------------------------------------------------------------------- /mails/templates/html_content.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | // Available variables: 4 | // 5 | // ``` 6 | // HtmlContent template.HTML 7 | // ``` 8 | const HtmlBody = `{{define "content"}}{{.HtmlContent}}{{end}}` 9 | -------------------------------------------------------------------------------- /mails/templates/password_login_alert.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | // Available variables: 4 | // 5 | // ``` 6 | // Record *models.Record 7 | // AppName string 8 | // AppUrl string 9 | // ProviderNames []string 10 | // ``` 11 | const PasswordLoginAlertBody = ` 12 | {{define "content"}} 13 |

Hello,

14 |

15 | Just to let you know that someone has logged in to your {{.AppName}} account using a password while you already have 16 | OAuth2 17 | {{range $index, $provider := .ProviderNames }} 18 | {{if $index}}|{{end}} 19 | {{ $provider }} 20 | {{ end }} 21 | auth linked. 22 |

23 |

If you have recently signed in with a password, you may disregard this email.

24 |

If you don't recognize the above action, you should immediately change your {{.AppName}} account password.

25 |

26 | Thanks,
27 | {{.AppName}} team 28 |

29 | {{end}} 30 | ` 31 | -------------------------------------------------------------------------------- /migrations/1677152688_rename_authentik_to_oidc.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "github.com/pocketbase/dbx" 5 | ) 6 | 7 | // This migration replaces the "authentikAuth" setting with "oidc". 8 | func init() { 9 | AppMigrations.Register(func(db dbx.Builder) error { 10 | _, err := db.NewQuery(` 11 | UPDATE {{_params}} 12 | SET [[value]] = replace([[value]]::text, '"authentikAuth":', '"oidcAuth":')::json 13 | WHERE [[key]] = 'settings' 14 | `).Execute() 15 | 16 | return err 17 | }, func(db dbx.Builder) error { 18 | _, err := db.NewQuery(` 19 | UPDATE {{_params}} 20 | SET [[value]] = replace([[value]]::text, '"oidcAuth":', '"authentikAuth":')::json 21 | WHERE [[key]] = 'settings' 22 | `).Execute() 23 | 24 | return err 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /migrations/1685164450_check_fk.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "github.com/pocketbase/dbx" 5 | ) 6 | 7 | // Cleanup dangling deleted collections references 8 | // (see https://github.com/hylarucoder/rocketbase/discussions/2570). 9 | func init() { 10 | AppMigrations.Register(func(db dbx.Builder) error { 11 | _, err := db.NewQuery(` 12 | DELETE FROM {{_externalAuths}} 13 | WHERE [[collectionId]] NOT IN (SELECT [[id]] FROM {{_collections}}) 14 | `).Execute() 15 | 16 | return err 17 | }, func(db dbx.Builder) error { 18 | return nil 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /migrations/1689579878_renormalize_single_multiple_values.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "github.com/pocketbase/dbx" 5 | ) 6 | 7 | // Renormalizes old single and multiple values of MultiValuer fields (file, select, relation) 8 | // (see https://github.com/hylarucoder/rocketbase/issues/2930). 9 | func init() { 10 | AppMigrations.Register(func(db dbx.Builder) error { 11 | return normalizeMultivaluerFields(db) 12 | }, func(db dbx.Builder) error { 13 | return nil 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /migrations/1690319366_reset_null_values.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hylarucoder/rocketbase/daos" 7 | "github.com/hylarucoder/rocketbase/models" 8 | "github.com/hylarucoder/rocketbase/models/schema" 9 | "github.com/pocketbase/dbx" 10 | ) 11 | 12 | // Reset all previously inserted NULL values to the fields zero-default. 13 | func init() { 14 | AppMigrations.Register(func(db dbx.Builder) error { 15 | dao := daos.New(db) 16 | 17 | collections := []*models.Collection{} 18 | if err := dao.CollectionQuery().All(&collections); err != nil { 19 | return err 20 | } 21 | 22 | for _, collection := range collections { 23 | if collection.IsView() { 24 | continue 25 | } 26 | 27 | for _, f := range collection.Schema.Fields() { 28 | defaultVal := "''" 29 | 30 | switch f.Type { 31 | case schema.FieldTypeJson: 32 | continue 33 | case schema.FieldTypeBool: 34 | defaultVal = "FALSE" 35 | case schema.FieldTypeNumber: 36 | defaultVal = "0" 37 | default: 38 | if opt, ok := f.Options.(schema.MultiValuer); ok && opt.IsMultiple() { 39 | defaultVal = "'[]'" 40 | } 41 | } 42 | 43 | _, err := db.NewQuery(fmt.Sprintf( 44 | "UPDATE {{%s}} SET [[%s]] = %s WHERE [[%s]] IS NULL", 45 | collection.Name, 46 | f.Name, 47 | defaultVal, 48 | f.Name, 49 | )).Execute() 50 | if err != nil { 51 | return err 52 | } 53 | } 54 | } 55 | 56 | return nil 57 | }, nil) 58 | } 59 | -------------------------------------------------------------------------------- /migrations/1691747913_resave_views.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "github.com/hylarucoder/rocketbase/daos" 5 | "github.com/hylarucoder/rocketbase/models" 6 | "github.com/pocketbase/dbx" 7 | ) 8 | 9 | // Resave all view collections to ensure that the proper id normalization is applied. 10 | // (see https://github.com/hylarucoder/rocketbase/issues/3110) 11 | func init() { 12 | AppMigrations.Register(func(db dbx.Builder) error { 13 | dao := daos.New(db) 14 | 15 | collections, err := dao.FindCollectionsByType(models.CollectionTypeView) 16 | if err != nil { 17 | return nil 18 | } 19 | 20 | for _, collection := range collections { 21 | // ignore errors to allow users to adjust 22 | // the view queries after app start 23 | dao.SaveCollection(collection) 24 | } 25 | 26 | return nil 27 | }, nil) 28 | } 29 | -------------------------------------------------------------------------------- /migrations/1701496825_allow_single_oauth2_provider_in_multiple_auth_collections.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "github.com/pocketbase/dbx" 5 | ) 6 | 7 | // Fixes the unique _externalAuths constraint for old installations 8 | // to allow a single OAuth2 provider to be registered for different auth collections. 9 | func init() { 10 | AppMigrations.Register(func(db dbx.Builder) error { 11 | _, createErr := db.NewQuery("CREATE UNIQUE INDEX IF NOT EXISTS _externalAuths_collection_provider_idx on {{_externalAuths}} ([[collectionId]], [[provider]], [[providerId]])").Execute() 12 | if createErr != nil { 13 | return createErr 14 | } 15 | 16 | _, dropErr := db.NewQuery("DROP INDEX IF EXISTS _externalAuths_provider_providerId_idx").Execute() 17 | if dropErr != nil { 18 | return dropErr 19 | } 20 | 21 | return nil 22 | }, nil) 23 | } 24 | -------------------------------------------------------------------------------- /migrations/1702134272_set_default_json_max_size.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "github.com/hylarucoder/rocketbase/daos" 5 | "github.com/hylarucoder/rocketbase/models" 6 | "github.com/hylarucoder/rocketbase/models/schema" 7 | "github.com/pocketbase/dbx" 8 | ) 9 | 10 | // Update all collections with json fields to have a default MaxSize json field option. 11 | func init() { 12 | AppMigrations.Register(func(db dbx.Builder) error { 13 | dao := daos.New(db) 14 | 15 | // note: update even the view collections to prevent 16 | // unnecessary change detections during the automigrate 17 | collections := []*models.Collection{} 18 | if err := dao.CollectionQuery().All(&collections); err != nil { 19 | return err 20 | } 21 | 22 | for _, collection := range collections { 23 | var needSave bool 24 | 25 | for _, f := range collection.Schema.Fields() { 26 | if f.Type != schema.FieldTypeJson { 27 | continue 28 | } 29 | 30 | options, _ := f.Options.(*schema.JsonOptions) 31 | if options == nil { 32 | options = &schema.JsonOptions{} 33 | } 34 | options.MaxSize = 2000000 // 2mb 35 | f.Options = options 36 | needSave = true 37 | } 38 | 39 | if !needSave { 40 | continue 41 | } 42 | 43 | // save only the collection model without updating its records table 44 | if err := dao.Save(collection); err != nil { 45 | return err 46 | } 47 | } 48 | 49 | return nil 50 | }, nil) 51 | } 52 | -------------------------------------------------------------------------------- /migrations/logs/1677760279_uppsercase_method.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "github.com/pocketbase/dbx" 5 | ) 6 | 7 | // This migration normalizes the request logs method to UPPERCASE (eg. "get" => "GET"). 8 | func init() { 9 | LogsMigrations.Register(func(db dbx.Builder) error { 10 | _, err := db.NewQuery("UPDATE {{_requests}} SET method=UPPER(method)").Execute() 11 | 12 | return err 13 | }, func(db dbx.Builder) error { 14 | _, err := db.NewQuery("UPDATE {{_requests}} SET method=LOWER(method)").Execute() 15 | 16 | return err 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /models/backup_file_info.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/hylarucoder/rocketbase/tools/types" 4 | 5 | type BackupFileInfo struct { 6 | Key string `json:"key"` 7 | Size int64 `json:"size"` 8 | Modified types.DateTime `json:"modified"` 9 | } 10 | -------------------------------------------------------------------------------- /models/external_auth.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | var _ Model = (*ExternalAuth)(nil) 4 | 5 | type ExternalAuth struct { 6 | BaseModel 7 | 8 | CollectionId string `db:"collectionId" json:"collectionId"` 9 | RecordId string `db:"recordId" json:"recordId"` 10 | Provider string `db:"provider" json:"provider"` 11 | ProviderId string `db:"providerId" json:"providerId"` 12 | } 13 | 14 | func (m *ExternalAuth) TableName() string { 15 | return "_externalAuths" 16 | } 17 | -------------------------------------------------------------------------------- /models/external_auth_test.go: -------------------------------------------------------------------------------- 1 | package models_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hylarucoder/rocketbase/models" 7 | ) 8 | 9 | func TestExternalAuthTableName(t *testing.T) { 10 | t.Parallel() 11 | 12 | m := models.ExternalAuth{} 13 | if m.TableName() != "_externalAuths" { 14 | t.Fatalf("Unexpected table name, got %q", m.TableName()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /models/log.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/hylarucoder/rocketbase/tools/types" 5 | ) 6 | 7 | var _ Model = (*Log)(nil) 8 | 9 | type Log struct { 10 | BaseModel 11 | 12 | Data types.JsonMap `db:"data" json:"data"` 13 | Message string `db:"message" json:"message"` 14 | Level int `db:"level" json:"level"` 15 | } 16 | 17 | func (m *Log) TableName() string { 18 | return "_logs" 19 | } 20 | -------------------------------------------------------------------------------- /models/param.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/hylarucoder/rocketbase/tools/types" 5 | ) 6 | 7 | var _ Model = (*Param)(nil) 8 | 9 | const ( 10 | ParamAppSettings = "settings" 11 | ) 12 | 13 | type Param struct { 14 | BaseModel 15 | 16 | Key string `db:"key" json:"key"` 17 | Value types.JsonRaw `db:"value" json:"value"` 18 | } 19 | 20 | func (m *Param) TableName() string { 21 | return "_params" 22 | } 23 | -------------------------------------------------------------------------------- /models/param_test.go: -------------------------------------------------------------------------------- 1 | package models_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hylarucoder/rocketbase/models" 7 | ) 8 | 9 | func TestParamTableName(t *testing.T) { 10 | t.Parallel() 11 | 12 | m := models.Param{} 13 | if m.TableName() != "_params" { 14 | t.Fatalf("Unexpected table name, got %q", m.TableName()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /models/request.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/hylarucoder/rocketbase/tools/types" 4 | 5 | var _ Model = (*Request)(nil) 6 | 7 | // list with the supported values for `Request.Auth` 8 | const ( 9 | RequestAuthGuest = "guest" 10 | RequestAuthAdmin = "admin" 11 | RequestAuthRecord = "authRecord" 12 | ) 13 | 14 | // Deprecated: Replaced by the Log model and will be removed in a future version. 15 | type Request struct { 16 | BaseModel 17 | 18 | Url string `db:"url" json:"url"` 19 | Method string `db:"method" json:"method"` 20 | Status int `db:"status" json:"status"` 21 | Auth string `db:"auth" json:"auth"` 22 | UserIp string `db:"userIp" json:"userIp"` 23 | RemoteIp string `db:"remoteIp" json:"remoteIp"` 24 | Referer string `db:"referer" json:"referer"` 25 | UserAgent string `db:"userAgent" json:"userAgent"` 26 | Meta types.JsonMap `db:"meta" json:"meta"` 27 | } 28 | 29 | func (m *Request) TableName() string { 30 | return "_requests" 31 | } 32 | -------------------------------------------------------------------------------- /models/request_info.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/hylarucoder/rocketbase/models/schema" 7 | ) 8 | 9 | const ( 10 | RequestInfoContextDefault = "default" 11 | RequestInfoContextRealtime = "realtime" 12 | RequestInfoContextProtectedFile = "protectedFile" 13 | RequestInfoContextOAuth2 = "oauth2" 14 | ) 15 | 16 | // RequestInfo defines a HTTP request data struct, usually used 17 | // as part of the `@request.*` filter resolver. 18 | type RequestInfo struct { 19 | Context string `json:"context"` 20 | Query map[string]any `json:"query"` 21 | Data map[string]any `json:"data"` 22 | Headers map[string]any `json:"headers"` 23 | AuthRecord *Record `json:"authRecord"` 24 | Admin *Admin `json:"admin"` 25 | Method string `json:"method"` 26 | } 27 | 28 | // HasModifierDataKeys loosely checks if the current struct has any modifier Data keys. 29 | func (r *RequestInfo) HasModifierDataKeys() bool { 30 | allModifiers := schema.FieldValueModifiers() 31 | 32 | for key := range r.Data { 33 | for _, m := range allModifiers { 34 | if strings.HasSuffix(key, m) { 35 | return true 36 | } 37 | } 38 | } 39 | 40 | return false 41 | } 42 | -------------------------------------------------------------------------------- /models/request_info_test.go: -------------------------------------------------------------------------------- 1 | package models_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hylarucoder/rocketbase/models" 7 | ) 8 | 9 | func TestRequestInfoHasModifierDataKeys(t *testing.T) { 10 | t.Parallel() 11 | 12 | scenarios := []struct { 13 | name string 14 | requestInfo *models.RequestInfo 15 | expected bool 16 | }{ 17 | { 18 | "empty", 19 | &models.RequestInfo{}, 20 | false, 21 | }, 22 | { 23 | "Data with regular fields", 24 | &models.RequestInfo{ 25 | Query: map[string]any{"data+": "demo"}, // should be ignored 26 | Data: map[string]any{"a": 123, "b": "test", "c.d": false}, 27 | }, 28 | false, 29 | }, 30 | { 31 | "Data with +modifier fields", 32 | &models.RequestInfo{ 33 | Data: map[string]any{"a+": 123, "b": "test", "c.d": false}, 34 | }, 35 | true, 36 | }, 37 | { 38 | "Data with -modifier fields", 39 | &models.RequestInfo{ 40 | Data: map[string]any{"a": 123, "b-": "test", "c.d": false}, 41 | }, 42 | true, 43 | }, 44 | { 45 | "Data with mixed modifier fields", 46 | &models.RequestInfo{ 47 | Data: map[string]any{"a": 123, "b-": "test", "c.d+": false}, 48 | }, 49 | true, 50 | }, 51 | } 52 | 53 | for _, s := range scenarios { 54 | result := s.requestInfo.HasModifierDataKeys() 55 | 56 | if result != s.expected { 57 | t.Fatalf("[%s] Expected %v, got %v", s.name, s.expected, result) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /models/request_test.go: -------------------------------------------------------------------------------- 1 | package models_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hylarucoder/rocketbase/models" 7 | ) 8 | 9 | func TestRequestTableName(t *testing.T) { 10 | m := models.Request{} 11 | if m.TableName() != "_requests" { 12 | t.Fatalf("Unexpected table name, got %q", m.TableName()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /models/table_info.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/hylarucoder/rocketbase/tools/types" 4 | 5 | type TableInfoRow struct { 6 | // the `db:"pk"` tag has special semantic so we cannot rename 7 | // the original field without specifying a custom mapper 8 | PK int 9 | 10 | Index int `db:"cid"` 11 | Name string `db:"column_name"` 12 | Type string `db:"type"` 13 | NotNull bool `db:"notnull"` 14 | DefaultValue types.JsonRaw `db:"column_default"` 15 | } 16 | -------------------------------------------------------------------------------- /plugins/ghupdate/ghupdate_test.go: -------------------------------------------------------------------------------- 1 | package ghupdate 2 | 3 | import "testing" 4 | 5 | func TestCompareVersions(t *testing.T) { 6 | scenarios := []struct { 7 | a string 8 | b string 9 | expected int 10 | }{ 11 | {"", "", 0}, 12 | {"0", "", 0}, 13 | {"1", "1.0.0", 0}, 14 | {"1.1", "1.1.0", 0}, 15 | {"1.1", "1.1.1", 1}, 16 | {"1.1", "1.0.1", -1}, 17 | {"1.0", "1.0.1", 1}, 18 | {"1.10", "1.9", -1}, 19 | {"1.2", "1.12", 1}, 20 | {"3.2", "1.6", -1}, 21 | {"0.0.2", "0.0.1", -1}, 22 | {"0.16.2", "0.17.0", 1}, 23 | {"1.15.0", "0.16.1", -1}, 24 | {"1.2.9", "1.2.10", 1}, 25 | {"3.2", "4.0", 1}, 26 | {"3.2.4", "3.2.3", -1}, 27 | } 28 | 29 | for i, s := range scenarios { 30 | result := compareVersions(s.a, s.b) 31 | 32 | if result != s.expected { 33 | t.Fatalf("[%d] Expected %q vs %q to result in %d, got %d", i, s.a, s.b, s.expected, result) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /plugins/ghupdate/release.go: -------------------------------------------------------------------------------- 1 | package ghupdate 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | type releaseAsset struct { 9 | Name string `json:"name"` 10 | DownloadUrl string `json:"browser_download_url"` 11 | Id int `json:"id"` 12 | Size int `json:"size"` 13 | } 14 | 15 | type release struct { 16 | Name string `json:"name"` 17 | Tag string `json:"tag_name"` 18 | Published string `json:"published_at"` 19 | Url string `json:"html_url"` 20 | Body string `json:"body"` 21 | Assets []*releaseAsset `json:"assets"` 22 | Id int `json:"id"` 23 | } 24 | 25 | // findAssetBySuffix returns the first available asset containing the specified suffix. 26 | func (r *release) findAssetBySuffix(suffix string) (*releaseAsset, error) { 27 | if suffix != "" { 28 | for _, asset := range r.Assets { 29 | if strings.HasSuffix(asset.Name, suffix) { 30 | return asset, nil 31 | } 32 | } 33 | } 34 | 35 | return nil, errors.New("missing asset containing " + suffix) 36 | } 37 | -------------------------------------------------------------------------------- /plugins/ghupdate/release_test.go: -------------------------------------------------------------------------------- 1 | package ghupdate 2 | 3 | import "testing" 4 | 5 | func TestReleaseFindAssetBySuffix(t *testing.T) { 6 | r := release{ 7 | Assets: []*releaseAsset{ 8 | {Name: "test1.zip", Id: 1}, 9 | {Name: "test2.zip", Id: 2}, 10 | {Name: "test22.zip", Id: 22}, 11 | {Name: "test3.zip", Id: 3}, 12 | }, 13 | } 14 | 15 | asset, err := r.findAssetBySuffix("2.zip") 16 | if err != nil { 17 | t.Fatalf("Expected nil, got err: %v", err) 18 | } 19 | 20 | if asset.Id != 2 { 21 | t.Fatalf("Expected asset with id %d, got %v", 2, asset) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugins/jsvm/internal/types/generated/embed.go: -------------------------------------------------------------------------------- 1 | package generated 2 | 3 | import "embed" 4 | 5 | //go:embed types.d.ts 6 | var Types embed.FS 7 | -------------------------------------------------------------------------------- /plugins/jsvm/mapper_test.go: -------------------------------------------------------------------------------- 1 | package jsvm_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/hylarucoder/rocketbase/plugins/jsvm" 8 | ) 9 | 10 | func TestFieldMapper(t *testing.T) { 11 | mapper := jsvm.FieldMapper{} 12 | 13 | scenarios := []struct { 14 | name string 15 | expected string 16 | }{ 17 | {"", ""}, 18 | {"test", "test"}, 19 | {"Test", "test"}, 20 | {"miXeD", "miXeD"}, 21 | {"MiXeD", "miXeD"}, 22 | {"ResolveRequestAsJSON", "resolveRequestAsJSON"}, 23 | {"Variable_with_underscore", "variable_with_underscore"}, 24 | {"ALLCAPS", "allcaps"}, 25 | {"ALL_CAPS_WITH_UNDERSCORE", "all_caps_with_underscore"}, 26 | {"OIDCMap", "oidcMap"}, 27 | {"MD5", "md5"}, 28 | } 29 | 30 | for i, s := range scenarios { 31 | field := reflect.StructField{Name: s.name} 32 | if v := mapper.FieldName(nil, field); v != s.expected { 33 | t.Fatalf("[%d] Expected FieldName %q, got %q", i, s.expected, v) 34 | } 35 | 36 | method := reflect.Method{Name: s.name} 37 | if v := mapper.MethodName(nil, method); v != s.expected { 38 | t.Fatalf("[%d] Expected MethodName %q, got %q", i, s.expected, v) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /resolvers/resolvers.go: -------------------------------------------------------------------------------- 1 | // Package resolvers contains custom search.FieldResolver implementations. 2 | package resolvers 3 | -------------------------------------------------------------------------------- /test_sh.sh: -------------------------------------------------------------------------------- 1 | # only TestAdminQuery 2 | go test -run TestAdminQuery ./... -------------------------------------------------------------------------------- /tests/auth_helper_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang-jwt/jwt/v4" 7 | "github.com/hylarucoder/rocketbase/tools/security" 8 | ) 9 | 10 | func TestRecordAuth(t *testing.T) { 11 | app, _ := NewTestApp() 12 | defer app.Cleanup() 13 | //record, _ := app.Dao().FindFirstRecordByFilter(""+ 14 | // "users", 15 | // `email = 'test@example.com'`, 16 | //) 17 | //record, _ := app.Dao().FindFirstRecordByFilter(""+ 18 | // "clients", 19 | // `email = 'test@example.com'`, 20 | //) 21 | //token, _ := security.NewJWT( 22 | // jwt.MapClaims{ 23 | // "id": record.Collection().Id, 24 | // "type": "authRecord", 25 | // "collectionId": record.Id, 26 | // "verified": true, 27 | // }, 28 | // record.TokenKey()+app.Settings().RecordAuthToken.Secret, 29 | // app.Settings().RecordAuthToken.Duration, 30 | //) 31 | record, _ := app.Dao().FindAdminByEmail("test@example.com") 32 | token, _ := security.NewJWT( 33 | jwt.MapClaims{ 34 | "id": record.Id, 35 | "type": "admin", 36 | }, 37 | record.TokenKey+app.Settings().AdminAuthToken.Secret, 38 | app.Settings().AdminAuthToken.Duration, 39 | ) 40 | println("token =>", token) 41 | } 42 | -------------------------------------------------------------------------------- /tests/data/.gitignore: -------------------------------------------------------------------------------- 1 | *.db-shm 2 | *.db-wal 3 | types.d.ts 4 | -------------------------------------------------------------------------------- /tests/data/empty.file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/empty.file -------------------------------------------------------------------------------- /tests/data/storage/9n89pl5vkct6330/la4y2w4o98acwuj/300_uh_lkx91_hvb_Da8K5pl069.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/9n89pl5vkct6330/la4y2w4o98acwuj/300_uh_lkx91_hvb_Da8K5pl069.png -------------------------------------------------------------------------------- /tests/data/storage/9n89pl5vkct6330/la4y2w4o98acwuj/300_uh_lkx91_hvb_Da8K5pl069.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":{"original-filename":"300_UhLKX91HVb.png"},"md5":"zZhZjzVvCvpcxtMAJie3GQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/9n89pl5vkct6330/la4y2w4o98acwuj/thumbs_300_uh_lkx91_hvb_Da8K5pl069.png/100x100_300_uh_lkx91_hvb_Da8K5pl069.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/9n89pl5vkct6330/la4y2w4o98acwuj/thumbs_300_uh_lkx91_hvb_Da8K5pl069.png/100x100_300_uh_lkx91_hvb_Da8K5pl069.png -------------------------------------------------------------------------------- /tests/data/storage/9n89pl5vkct6330/la4y2w4o98acwuj/thumbs_300_uh_lkx91_hvb_Da8K5pl069.png/100x100_300_uh_lkx91_hvb_Da8K5pl069.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"R7TqfvF8HP3C4+FO2eZ9tg=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/9n89pl5vkct6330/qjeql998mtp1azp/logo_vcf_jjg5_tah_9MtIHytOmZ.svg.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/svg+xml","user.metadata":{"original-filename":"logo_vcfJJG5TAh.svg"},"md5":"9/B7afas4c3O6vbFcbpOug=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"zZhZjzVvCvpcxtMAJie3GQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/0x50_300_1SEi6Q6U72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/0x50_300_1SEi6Q6U72.png -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/0x50_300_1SEi6Q6U72.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"iqCiUST0LvGibMMM1qxZAA=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/100x100_300_1SEi6Q6U72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/100x100_300_1SEi6Q6U72.png -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/100x100_300_1SEi6Q6U72.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"R7TqfvF8HP3C4+FO2eZ9tg=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x0_300_1SEi6Q6U72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x0_300_1SEi6Q6U72.png -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x0_300_1SEi6Q6U72.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"XQUKRr4ZwZ9MTo2kR+KfIg=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x50_300_1SEi6Q6U72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x50_300_1SEi6Q6U72.png -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x50_300_1SEi6Q6U72.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"EoyFICWlQdYZgUYTEMsp/A=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x50b_300_1SEi6Q6U72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x50b_300_1SEi6Q6U72.png -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x50b_300_1SEi6Q6U72.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"Pb/AI46vKOtcBW9bOsdREA=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x50f_300_1SEi6Q6U72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x50f_300_1SEi6Q6U72.png -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x50f_300_1SEi6Q6U72.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"iqCiUST0LvGibMMM1qxZAA=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x50t_300_1SEi6Q6U72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x50t_300_1SEi6Q6U72.png -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/4q1xlclmfloku33/thumbs_300_1SEi6Q6U72.png/70x50t_300_1SEi6Q6U72.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"rcwrlxKlvwTgDpsHLHzBqA=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/oap640cot4yru2s/test_kfd2wYLxkz.txt: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /tests/data/storage/_pb_users_auth_/oap640cot4yru2s/test_kfd2wYLxkz.txt.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"text/plain; charset=utf-8","user.metadata":null,"md5":"2Oj8otwPiW/Xy0ywAxuiSQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/300_WlbFWSGmW9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/300_WlbFWSGmW9.png -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/300_WlbFWSGmW9.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"zZhZjzVvCvpcxtMAJie3GQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/logo_vcfJJG5TAh.svg.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/svg+xml","user.metadata":null,"md5":"9/B7afas4c3O6vbFcbpOug=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/test_MaWC6mWyrP.txt: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/test_MaWC6mWyrP.txt.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"text/plain; charset=utf-8","user.metadata":{"original-filename":"test.txt"},"md5":"2Oj8otwPiW/Xy0ywAxuiSQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/test_QZFjKjXchk.txt: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/test_QZFjKjXchk.txt.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"text/plain; charset=utf-8","user.metadata":null,"md5":"2Oj8otwPiW/Xy0ywAxuiSQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/test_d61b33QdDU.txt: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/test_d61b33QdDU.txt.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"text/plain; charset=utf-8","user.metadata":null,"md5":"2Oj8otwPiW/Xy0ywAxuiSQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/test_tC1Yc87DfC.txt: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/test_tC1Yc87DfC.txt.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"text/plain; charset=utf-8","user.metadata":{"original-filename":"test.txt"},"md5":"2Oj8otwPiW/Xy0ywAxuiSQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/thumbs_300_WlbFWSGmW9.png/100x100_300_WlbFWSGmW9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/thumbs_300_WlbFWSGmW9.png/100x100_300_WlbFWSGmW9.png -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/84nmscqy84lsi1t/thumbs_300_WlbFWSGmW9.png/100x100_300_WlbFWSGmW9.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"R7TqfvF8HP3C4+FO2eZ9tg=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/wsmn24bux7wo113/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"zZhZjzVvCvpcxtMAJie3GQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/al1h9ijdeojtsjy/thumbs_300_Jsjq7RdBgA.png/100x100_300_Jsjq7RdBgA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/wsmn24bux7wo113/al1h9ijdeojtsjy/thumbs_300_Jsjq7RdBgA.png/100x100_300_Jsjq7RdBgA.png -------------------------------------------------------------------------------- /tests/data/storage/wsmn24bux7wo113/al1h9ijdeojtsjy/thumbs_300_Jsjq7RdBgA.png/100x100_300_Jsjq7RdBgA.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"R7TqfvF8HP3C4+FO2eZ9tg=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wzlqyes4orhoygb/7nwo8tuiatetxdm/test_JnXeKEwgwr.txt: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /tests/data/storage/wzlqyes4orhoygb/7nwo8tuiatetxdm/test_JnXeKEwgwr.txt.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"text/plain; charset=utf-8","user.metadata":null,"md5":"2Oj8otwPiW/Xy0ywAxuiSQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wzlqyes4orhoygb/lcl9d87w22ml6jy/300_UhLKX91HVb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/wzlqyes4orhoygb/lcl9d87w22ml6jy/300_UhLKX91HVb.png -------------------------------------------------------------------------------- /tests/data/storage/wzlqyes4orhoygb/lcl9d87w22ml6jy/300_UhLKX91HVb.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"zZhZjzVvCvpcxtMAJie3GQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wzlqyes4orhoygb/lcl9d87w22ml6jy/test_FLurQTgrY8.txt: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /tests/data/storage/wzlqyes4orhoygb/lcl9d87w22ml6jy/test_FLurQTgrY8.txt.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"text/plain; charset=utf-8","user.metadata":null,"md5":"2Oj8otwPiW/Xy0ywAxuiSQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wzlqyes4orhoygb/lcl9d87w22ml6jy/thumbs_300_UhLKX91HVb.png/100x100_300_UhLKX91HVb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/wzlqyes4orhoygb/lcl9d87w22ml6jy/thumbs_300_UhLKX91HVb.png/100x100_300_UhLKX91HVb.png -------------------------------------------------------------------------------- /tests/data/storage/wzlqyes4orhoygb/lcl9d87w22ml6jy/thumbs_300_UhLKX91HVb.png/100x100_300_UhLKX91HVb.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"R7TqfvF8HP3C4+FO2eZ9tg=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wzlqyes4orhoygb/mk5fmymtx4wsprk/300_JdfBOieXAW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/wzlqyes4orhoygb/mk5fmymtx4wsprk/300_JdfBOieXAW.png -------------------------------------------------------------------------------- /tests/data/storage/wzlqyes4orhoygb/mk5fmymtx4wsprk/300_JdfBOieXAW.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"zZhZjzVvCvpcxtMAJie3GQ=="} 2 | -------------------------------------------------------------------------------- /tests/data/storage/wzlqyes4orhoygb/mk5fmymtx4wsprk/thumbs_300_JdfBOieXAW.png/100x100_300_JdfBOieXAW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/tests/data/storage/wzlqyes4orhoygb/mk5fmymtx4wsprk/thumbs_300_JdfBOieXAW.png/100x100_300_JdfBOieXAW.png -------------------------------------------------------------------------------- /tests/data/storage/wzlqyes4orhoygb/mk5fmymtx4wsprk/thumbs_300_JdfBOieXAW.png/100x100_300_JdfBOieXAW.png.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"R7TqfvF8HP3C4+FO2eZ9tg=="} 2 | -------------------------------------------------------------------------------- /tests/logs.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | func MockLogsData(app *TestApp) error { 4 | _, err := app.LogsDB().NewQuery(` 5 | delete from {{_logs}}; 6 | 7 | insert into {{_logs}} ( 8 | [[id]], 9 | [[level]], 10 | [[message]], 11 | [[data]], 12 | [[created]], 13 | [[updated]] 14 | ) 15 | values 16 | ( 17 | "873f2133-9f38-44fb-bf82-c8f53b310d91", 18 | 0, 19 | "test_message1", 20 | '{"status":200}', 21 | "2022-05-01 10:00:00.123Z", 22 | "2022-05-01 10:00:00.123Z" 23 | ), 24 | ( 25 | "f2133873-44fb-9f38-bf82-c918f53b310d", 26 | 8, 27 | "test_message2", 28 | '{"status":400}', 29 | "2022-05-02 10:00:00.123Z", 30 | "2022-05-02 10:00:00.123Z" 31 | ); 32 | `).Execute() 33 | 34 | return err 35 | } 36 | -------------------------------------------------------------------------------- /tests/mailer.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "github.com/hylarucoder/rocketbase/tools/mailer" 5 | "sync" 6 | ) 7 | 8 | var _ mailer.Mailer = (*TestMailer)(nil) 9 | 10 | // TestMailer is a mock `mailer.Mailer` implementation. 11 | type TestMailer struct { 12 | mux sync.Mutex 13 | 14 | TotalSend int 15 | LastMessage mailer.Message 16 | 17 | // @todo consider deprecating the above 2 fields? 18 | SentMessages []mailer.Message 19 | } 20 | 21 | // Reset clears any previously test collected data. 22 | func (tm *TestMailer) Reset() { 23 | tm.mux.Lock() 24 | defer tm.mux.Unlock() 25 | 26 | tm.TotalSend = 0 27 | tm.LastMessage = mailer.Message{} 28 | tm.SentMessages = nil 29 | } 30 | 31 | // Send implements `mailer.Mailer` interface. 32 | func (tm *TestMailer) Send(m *mailer.Message) error { 33 | tm.mux.Lock() 34 | defer tm.mux.Unlock() 35 | 36 | tm.TotalSend++ 37 | tm.LastMessage = *m 38 | tm.SentMessages = append(tm.SentMessages, tm.LastMessage) 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /tests/request.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "mime/multipart" 7 | "os" 8 | ) 9 | 10 | // MockMultipartData creates a mocked multipart/form-data payload. 11 | // 12 | // Example 13 | // 14 | // data, mp, err := tests.MockMultipartData( 15 | // map[string]string{"title": "new"}, 16 | // "file1", 17 | // "file2", 18 | // ... 19 | // ) 20 | func MockMultipartData(data map[string]string, fileFields ...string) (*bytes.Buffer, *multipart.Writer, error) { 21 | body := new(bytes.Buffer) 22 | mp := multipart.NewWriter(body) 23 | defer mp.Close() 24 | 25 | // write data fields 26 | for k, v := range data { 27 | mp.WriteField(k, v) 28 | } 29 | 30 | // write file fields 31 | for _, fileField := range fileFields { 32 | // create a test temporary file 33 | err := func() error { 34 | tmpFile, err := os.CreateTemp(os.TempDir(), "tmpfile-*.txt") 35 | if err != nil { 36 | return err 37 | } 38 | 39 | if _, err := tmpFile.Write([]byte("test")); err != nil { 40 | return err 41 | } 42 | tmpFile.Seek(0, 0) 43 | defer tmpFile.Close() 44 | defer os.Remove(tmpFile.Name()) 45 | 46 | // stub uploaded file 47 | w, err := mp.CreateFormFile(fileField, tmpFile.Name()) 48 | if err != nil { 49 | return err 50 | } 51 | if _, err := io.Copy(w, tmpFile); err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | }() 57 | if err != nil { 58 | return nil, nil, err 59 | } 60 | } 61 | 62 | return body, mp, nil 63 | } 64 | -------------------------------------------------------------------------------- /tokens/admin.go: -------------------------------------------------------------------------------- 1 | package tokens 2 | 3 | import ( 4 | "github.com/golang-jwt/jwt/v4" 5 | "github.com/hylarucoder/rocketbase/core" 6 | "github.com/hylarucoder/rocketbase/models" 7 | "github.com/hylarucoder/rocketbase/tools/security" 8 | ) 9 | 10 | // NewAdminAuthToken generates and returns a new admin authentication token. 11 | func NewAdminAuthToken(app core.App, admin *models.Admin) (string, error) { 12 | return security.NewJWT( 13 | jwt.MapClaims{"id": admin.Id, "type": TypeAdmin}, 14 | (admin.TokenKey + app.Settings().AdminAuthToken.Secret), 15 | app.Settings().AdminAuthToken.Duration, 16 | ) 17 | } 18 | 19 | // NewAdminResetPasswordToken generates and returns a new admin password reset request token. 20 | func NewAdminResetPasswordToken(app core.App, admin *models.Admin) (string, error) { 21 | return security.NewJWT( 22 | jwt.MapClaims{"id": admin.Id, "type": TypeAdmin, "email": admin.Email}, 23 | (admin.TokenKey + app.Settings().AdminPasswordResetToken.Secret), 24 | app.Settings().AdminPasswordResetToken.Duration, 25 | ) 26 | } 27 | 28 | // NewAdminFileToken generates and returns a new admin private file access token. 29 | func NewAdminFileToken(app core.App, admin *models.Admin) (string, error) { 30 | return security.NewJWT( 31 | jwt.MapClaims{"id": admin.Id, "type": TypeAdmin}, 32 | (admin.TokenKey + app.Settings().AdminFileToken.Secret), 33 | app.Settings().AdminFileToken.Duration, 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /tokens/tokens.go: -------------------------------------------------------------------------------- 1 | // Package tokens implements various user and admin tokens generation methods. 2 | package tokens 3 | 4 | const ( 5 | TypeAdmin = "admin" 6 | TypeAuthRecord = "authRecord" 7 | ) 8 | -------------------------------------------------------------------------------- /tools/logger/log.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "log/slog" 5 | "time" 6 | 7 | "github.com/hylarucoder/rocketbase/tools/types" 8 | ) 9 | 10 | // Log is similar to [slog.Record] bit contains the log attributes as 11 | // preformatted JSON map. 12 | type Log struct { 13 | Time time.Time 14 | Message string 15 | Level slog.Level 16 | Data types.JsonMap 17 | } 18 | -------------------------------------------------------------------------------- /tools/mailer/mailer.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "io" 5 | "net/mail" 6 | ) 7 | 8 | // Message defines a generic email message struct. 9 | type Message struct { 10 | From mail.Address `json:"from"` 11 | To []mail.Address `json:"to"` 12 | Bcc []mail.Address `json:"bcc"` 13 | Cc []mail.Address `json:"cc"` 14 | Subject string `json:"subject"` 15 | HTML string `json:"html"` 16 | Text string `json:"text"` 17 | Headers map[string]string `json:"headers"` 18 | Attachments map[string]io.Reader `json:"attachments"` 19 | } 20 | 21 | // Mailer defines a base mail client interface. 22 | type Mailer interface { 23 | // Send sends an email with the provided Message. 24 | Send(message *Message) error 25 | } 26 | 27 | // addressesToStrings converts the provided address to a list of serialized RFC 5322 strings. 28 | // 29 | // To export only the email part of mail.Address, you can set withName to false. 30 | func addressesToStrings(addresses []mail.Address, withName bool) []string { 31 | result := make([]string, len(addresses)) 32 | 33 | for i, addr := range addresses { 34 | if withName && addr.Name != "" { 35 | result[i] = addr.String() 36 | } else { 37 | // keep only the email part to avoid wrapping in angle-brackets 38 | result[i] = addr.Address 39 | } 40 | } 41 | 42 | return result 43 | } 44 | -------------------------------------------------------------------------------- /tools/migrate/list_test.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMigrationsList(t *testing.T) { 8 | l := MigrationsList{} 9 | 10 | l.Register(nil, nil, "3_test.go") 11 | l.Register(nil, nil, "1_test.go") 12 | l.Register(nil, nil, "2_test.go") 13 | l.Register(nil, nil /* auto detect file name */) 14 | 15 | expected := []string{ 16 | "1_test.go", 17 | "2_test.go", 18 | "3_test.go", 19 | "list_test.go", 20 | } 21 | 22 | items := l.Items() 23 | if len(items) != len(expected) { 24 | t.Fatalf("Expected %d items, got %d: \n%#v", len(expected), len(items), items) 25 | } 26 | 27 | for i, name := range expected { 28 | item := l.Item(i) 29 | if item.File != name { 30 | t.Fatalf("Expected name %s for index %d, got %s", name, i, item.File) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tools/rest/uploaded_file.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/hylarucoder/rocketbase/tools/filesystem" 7 | ) 8 | 9 | // DefaultMaxMemory defines the default max memory bytes that 10 | // will be used when parsing a form request body. 11 | const DefaultMaxMemory = 32 << 20 // 32mb 12 | 13 | // FindUploadedFiles extracts all form files of "key" from a http request 14 | // and returns a slice with filesystem.File instances (if any). 15 | func FindUploadedFiles(r *http.Request, key string) ([]*filesystem.File, error) { 16 | if r.MultipartForm == nil { 17 | err := r.ParseMultipartForm(DefaultMaxMemory) 18 | if err != nil { 19 | return nil, err 20 | } 21 | } 22 | 23 | if r.MultipartForm == nil || r.MultipartForm.File == nil || len(r.MultipartForm.File[key]) == 0 { 24 | return nil, http.ErrMissingFile 25 | } 26 | 27 | result := make([]*filesystem.File, 0, len(r.MultipartForm.File[key])) 28 | 29 | for _, fh := range r.MultipartForm.File[key] { 30 | file, err := filesystem.NewFileFromMultipart(fh) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | result = append(result, file) 36 | } 37 | 38 | return result, nil 39 | } 40 | -------------------------------------------------------------------------------- /tools/rest/url.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "net/url" 5 | "path" 6 | "strings" 7 | ) 8 | 9 | // NormalizeUrl removes duplicated slashes from a url path. 10 | func NormalizeUrl(originalUrl string) (string, error) { 11 | u, err := url.Parse(originalUrl) 12 | if err != nil { 13 | return "", err 14 | } 15 | 16 | hasSlash := strings.HasSuffix(u.Path, "/") 17 | 18 | // clean up path by removing duplicated / 19 | u.Path = path.Clean(u.Path) 20 | u.RawPath = path.Clean(u.RawPath) 21 | 22 | // restore original trailing slash 23 | if hasSlash && !strings.HasSuffix(u.Path, "/") { 24 | u.Path += "/" 25 | u.RawPath += "/" 26 | } 27 | 28 | return u.String(), nil 29 | } 30 | -------------------------------------------------------------------------------- /tools/rest/url_test.go: -------------------------------------------------------------------------------- 1 | package rest_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hylarucoder/rocketbase/tools/rest" 7 | ) 8 | 9 | func TestNormalizeUrl(t *testing.T) { 10 | scenarios := []struct { 11 | url string 12 | expectError bool 13 | expectUrl string 14 | }{ 15 | {":/", true, ""}, 16 | {"./", false, "./"}, 17 | {"../../test////", false, "../../test/"}, 18 | {"/a/b/c", false, "/a/b/c"}, 19 | {"a/////b//c/", false, "a/b/c/"}, 20 | {"/a/////b//c", false, "/a/b/c"}, 21 | {"///a/b/c", false, "/a/b/c"}, 22 | {"//a/b/c", false, "//a/b/c"}, // preserve "auto-schema" 23 | {"http://a/b/c", false, "http://a/b/c"}, 24 | {"a//bc?test=1//dd", false, "a/bc?test=1//dd"}, // only the path is normalized 25 | {"a//bc?test=1#12///3", false, "a/bc?test=1#12///3"}, // only the path is normalized 26 | } 27 | 28 | for i, s := range scenarios { 29 | result, err := rest.NormalizeUrl(s.url) 30 | 31 | hasErr := err != nil 32 | if hasErr != s.expectError { 33 | t.Errorf("(%d) Expected hasErr %v, got %v", i, s.expectError, hasErr) 34 | } 35 | 36 | if result != s.expectUrl { 37 | t.Errorf("(%d) Expected url %q, got %q", i, s.expectUrl, result) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tools/routine/routine.go: -------------------------------------------------------------------------------- 1 | package routine 2 | 3 | import ( 4 | "log" 5 | "runtime/debug" 6 | "sync" 7 | ) 8 | 9 | // FireAndForget executes `f()` in a new go routine and auto recovers if panic. 10 | // 11 | // **Note:** Use this only if you are not interested in the result of `f()` 12 | // and don't want to block the parent go routine. 13 | func FireAndForget(f func(), wg ...*sync.WaitGroup) { 14 | if len(wg) > 0 && wg[0] != nil { 15 | wg[0].Add(1) 16 | } 17 | 18 | go func() { 19 | if len(wg) > 0 && wg[0] != nil { 20 | defer wg[0].Done() 21 | } 22 | 23 | defer func() { 24 | if err := recover(); err != nil { 25 | log.Printf("RECOVERED FROM PANIC (safe to ignore): %v", err) 26 | log.Println(string(debug.Stack())) 27 | } 28 | }() 29 | 30 | f() 31 | }() 32 | } 33 | -------------------------------------------------------------------------------- /tools/routine/routine_test.go: -------------------------------------------------------------------------------- 1 | package routine_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/hylarucoder/rocketbase/tools/routine" 8 | ) 9 | 10 | func TestFireAndForget(t *testing.T) { 11 | called := false 12 | 13 | fn := func() { 14 | called = true 15 | panic("test") 16 | } 17 | 18 | wg := &sync.WaitGroup{} 19 | 20 | routine.FireAndForget(fn, wg) 21 | 22 | wg.Wait() 23 | 24 | if !called { 25 | t.Error("Expected fn to be called.") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tools/search/identifier_macros_test.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestIdentifierMacros(t *testing.T) { 9 | originalTimeNow := timeNow 10 | 11 | timeNow = func() time.Time { 12 | return time.Date(2023, 2, 3, 4, 5, 6, 7, time.UTC) 13 | } 14 | 15 | testMacros := map[string]any{ 16 | "@now": "2023-02-03 04:05:06.000Z", 17 | "@second": 6, 18 | "@minute": 5, 19 | "@hour": 4, 20 | "@day": 3, 21 | "@month": 2, 22 | "@weekday": 5, 23 | "@year": 2023, 24 | "@todayStart": "2023-02-03 00:00:00.000Z", 25 | "@todayEnd": "2023-02-03 23:59:59.999Z", 26 | "@monthStart": "2023-02-01 00:00:00.000Z", 27 | "@monthEnd": "2023-02-28 23:59:59.999Z", 28 | "@yearStart": "2023-01-01 00:00:00.000Z", 29 | "@yearEnd": "2023-12-31 23:59:59.999Z", 30 | } 31 | 32 | if len(testMacros) != len(identifierMacros) { 33 | t.Fatalf("Expected %d macros, got %d", len(testMacros), len(identifierMacros)) 34 | } 35 | 36 | for key, expected := range testMacros { 37 | t.Run(key, func(t *testing.T) { 38 | macro, ok := identifierMacros[key] 39 | if !ok { 40 | t.Fatalf("Missing macro %s", key) 41 | } 42 | 43 | result, err := macro() 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | if result != expected { 49 | t.Fatalf("Expected %q, got %q", expected, result) 50 | } 51 | }) 52 | } 53 | 54 | // restore 55 | timeNow = originalTimeNow 56 | } 57 | -------------------------------------------------------------------------------- /tools/template/renderer.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "html/template" 7 | ) 8 | 9 | // Renderer defines a single parsed template. 10 | type Renderer struct { 11 | template *template.Template 12 | parseError error 13 | } 14 | 15 | // Render executes the template with the specified data as the dot object 16 | // and returns the result as plain string. 17 | func (r *Renderer) Render(data any) (string, error) { 18 | if r.parseError != nil { 19 | return "", r.parseError 20 | } 21 | 22 | if r.template == nil { 23 | return "", errors.New("invalid or nil template") 24 | } 25 | 26 | buf := new(bytes.Buffer) 27 | 28 | if err := r.template.Execute(buf, data); err != nil { 29 | return "", err 30 | } 31 | 32 | return buf.String(), nil 33 | } 34 | -------------------------------------------------------------------------------- /tools/template/renderer_test.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "errors" 5 | "html/template" 6 | "testing" 7 | ) 8 | 9 | func TestRendererRender(t *testing.T) { 10 | tpl, _ := template.New("").Parse("Hello {{.Name}}!") 11 | tpl.Option("missingkey=error") // enforce execute errors 12 | 13 | scenarios := map[string]struct { 14 | renderer *Renderer 15 | data any 16 | expectedHasErr bool 17 | expectedResult string 18 | }{ 19 | "with nil template": { 20 | &Renderer{}, 21 | nil, 22 | true, 23 | "", 24 | }, 25 | "with parse error": { 26 | &Renderer{ 27 | template: tpl, 28 | parseError: errors.New("test"), 29 | }, 30 | nil, 31 | true, 32 | "", 33 | }, 34 | "with execute error": { 35 | &Renderer{template: tpl}, 36 | nil, 37 | true, 38 | "", 39 | }, 40 | "no error": { 41 | &Renderer{template: tpl}, 42 | struct{ Name string }{"world"}, 43 | false, 44 | "Hello world!", 45 | }, 46 | } 47 | 48 | for name, s := range scenarios { 49 | t.Run(name, func(t *testing.T) { 50 | result, err := s.renderer.Render(s.data) 51 | 52 | hasErr := err != nil 53 | 54 | if s.expectedHasErr != hasErr { 55 | t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectedHasErr, hasErr, err) 56 | } 57 | 58 | if s.expectedResult != result { 59 | t.Fatalf("Expected result %v, got %v", s.expectedResult, result) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tools/test_utils/test_utils.go: -------------------------------------------------------------------------------- 1 | package test_utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "sync" 8 | 9 | "github.com/joho/godotenv" 10 | ) 11 | 12 | var loadOnce sync.Once 13 | 14 | func LoadTestEnv(envFileName ...string) { 15 | loadOnce.Do(func() { 16 | fileName := ".env.test" 17 | if len(envFileName) > 0 { 18 | fileName = envFileName[0] 19 | } 20 | 21 | dir, err := os.Getwd() 22 | if err != nil { 23 | fmt.Printf("Error getting current working directory: %v\n", err) 24 | } else { 25 | for { 26 | envFile := filepath.Join(dir, fileName) 27 | if _, err := os.Stat(envFile); err == nil { 28 | if err := godotenv.Load(envFile); err != nil { 29 | fmt.Printf("Error loading %s file: %v\n", fileName, err) 30 | } else { 31 | fmt.Printf("Loaded %s from: %s\n", fileName, envFile) 32 | break 33 | } 34 | } 35 | 36 | parent := filepath.Dir(dir) 37 | if parent == dir { 38 | fmt.Printf("Reached root directory, %s not found\n", fileName) 39 | break 40 | } 41 | dir = parent 42 | } 43 | } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /tools/types/json_array.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | // JsonArray defines a slice that is safe for json and db read/write. 10 | type JsonArray[T any] []T 11 | 12 | // internal alias to prevent recursion during marshalization. 13 | type jsonArrayAlias[T any] JsonArray[T] 14 | 15 | // MarshalJSON implements the [json.Marshaler] interface. 16 | func (m JsonArray[T]) MarshalJSON() ([]byte, error) { 17 | // initialize an empty map to ensure that `[]` is returned as json 18 | if m == nil { 19 | m = JsonArray[T]{} 20 | } 21 | 22 | return json.Marshal(jsonArrayAlias[T](m)) 23 | } 24 | 25 | // Value implements the [driver.Valuer] interface. 26 | func (m JsonArray[T]) Value() (driver.Value, error) { 27 | data, err := json.Marshal(m) 28 | 29 | return string(data), err 30 | } 31 | 32 | // Scan implements [sql.Scanner] interface to scan the provided value 33 | // into the current JsonArray[T] instance. 34 | func (m *JsonArray[T]) Scan(value any) error { 35 | var data []byte 36 | switch v := value.(type) { 37 | case nil: 38 | // no cast needed 39 | case []byte: 40 | data = v 41 | case string: 42 | data = []byte(v) 43 | default: 44 | return fmt.Errorf("failed to unmarshal JsonArray value: %q", value) 45 | } 46 | 47 | if len(data) == 0 { 48 | data = []byte("[]") 49 | } 50 | 51 | return json.Unmarshal(data, m) 52 | } 53 | -------------------------------------------------------------------------------- /tools/types/types.go: -------------------------------------------------------------------------------- 1 | // Package types implements some commonly used db serializable types 2 | // like datetime, json, etc. 3 | package types 4 | 5 | // Pointer is a generic helper that returns val as *T. 6 | func Pointer[T any](val T) *T { 7 | return &val 8 | } 9 | -------------------------------------------------------------------------------- /tools/types/types_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hylarucoder/rocketbase/tools/types" 7 | ) 8 | 9 | func TestPointer(t *testing.T) { 10 | s1 := types.Pointer("") 11 | if s1 == nil || *s1 != "" { 12 | t.Fatalf("Expected empty string pointer, got %#v", s1) 13 | } 14 | 15 | s2 := types.Pointer("test") 16 | if s2 == nil || *s2 != "test" { 17 | t.Fatalf("Expected 'test' string pointer, got %#v", s2) 18 | } 19 | 20 | s3 := types.Pointer(123) 21 | if s3 == nil || *s3 != 123 { 22 | t.Fatalf("Expected 123 string pointer, got %#v", s3) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/.env: -------------------------------------------------------------------------------- 1 | # all environments should start with 'PB_' prefix 2 | PB_BACKEND_URL = "../" 3 | PB_INSTALLER_PARAM = "installer" 4 | PB_OAUTH2_EXAMPLE = "https://pocketbase.io/docs/authentication/#oauth2-integration" 5 | PB_RULES_SYNTAX_DOCS = "https://pocketbase.io/docs/api-rules-and-filters/" 6 | PB_FILE_UPLOAD_DOCS = "https://pocketbase.io/docs/files-handling/" 7 | PB_PROTECTED_FILE_DOCS = "https://pocketbase.io/docs/files-handling/#protected-files" 8 | PB_DOCS_URL = "https://pocketbase.io/docs/" 9 | PB_JS_SDK_URL = "https://github.com/pocketbase/js-sdk" 10 | PB_DART_SDK_URL = "https://github.com/pocketbase/dart-sdk" 11 | PB_RELEASES = "https://github.com/hylarucoder/rocketbase/releases" 12 | PB_VERSION = "v0.22.22" 13 | -------------------------------------------------------------------------------- /ui/.env.development: -------------------------------------------------------------------------------- 1 | PB_BACKEND_URL = "http://127.0.0.1:8090" 2 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.vscode/ 3 | .DS_Store 4 | 5 | # exclude local env files 6 | .env.local 7 | .env.*.local 8 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | PocketBase Admin dashboard UI 2 | ====================================================================== 3 | 4 | This is the PocketBase Admin dashboard UI (built with Svelte and Vite). 5 | 6 | Although it could be used independently, it is mainly intended to be embedded 7 | as part of a PocketBase app executable (hence the `embed.go` file). 8 | 9 | ## Development 10 | 11 | Download the repo and run the appropriate console commands: 12 | 13 | ```sh 14 | # install dependencies 15 | npm install 16 | 17 | # start a dev server with hot reload at localhost:3000 18 | npm run dev 19 | 20 | # or generate production ready bundle in dist/ directory 21 | npm run build 22 | ``` 23 | -------------------------------------------------------------------------------- /ui/dist/assets/ListApiDocs-ByASLUZu.css: -------------------------------------------------------------------------------- 1 | .filter-op.svelte-1w7s5nw{display:inline-block;vertical-align:top;margin-right:5px;width:30px;text-align:center;padding-left:0;padding-right:0} 2 | -------------------------------------------------------------------------------- /ui/dist/assets/PageOAuth2RedirectFailure-BS8Cmo1H.js: -------------------------------------------------------------------------------- 1 | import{S as o,i,s as c,e as r,f as l,g as u,y as a,o as d,I as h}from"./index-CH1esvLs.js";function f(n){let t;return{c(){t=r("div"),t.innerHTML='

Auth failed.

You can close this window and go back to the app to try again.
',l(t,"class","content txt-hint txt-center p-base")},m(e,s){u(e,t,s)},p:a,i:a,o:a,d(e){e&&d(t)}}}function p(n){return h(()=>{window.close()}),[]}class g extends o{constructor(t){super(),i(this,t,p,f,c,{})}}export{g as default}; 2 | -------------------------------------------------------------------------------- /ui/dist/assets/PageOAuth2RedirectSuccess-D1_hRUhp.js: -------------------------------------------------------------------------------- 1 | import{S as o,i as c,s as i,e as r,f as u,g as l,y as s,o as d,I as h}from"./index-CH1esvLs.js";function p(n){let t;return{c(){t=r("div"),t.innerHTML='

Auth completed.

You can close this window and go back to the app.
',u(t,"class","content txt-hint txt-center p-base")},m(e,a){l(e,t,a)},p:s,i:s,o:s,d(e){e&&d(t)}}}function f(n){return h(()=>{window.close()}),[]}class x extends o{constructor(t){super(),c(this,t,f,p,i,{})}}export{x as default}; 2 | -------------------------------------------------------------------------------- /ui/dist/assets/SdkTabs-CSWLpEgj.css: -------------------------------------------------------------------------------- 1 | .sdk-tabs.svelte-1maocj6 .tabs-header .tab-item.svelte-1maocj6{min-width:100px} 2 | -------------------------------------------------------------------------------- /ui/dist/fonts/remixicon/remixicon.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/fonts/remixicon/remixicon.woff2 -------------------------------------------------------------------------------- /ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff2 -------------------------------------------------------------------------------- /ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff2 -------------------------------------------------------------------------------- /ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff2 -------------------------------------------------------------------------------- /ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff2 -------------------------------------------------------------------------------- /ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff2 -------------------------------------------------------------------------------- /ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff2 -------------------------------------------------------------------------------- /ui/dist/fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-700.woff2 -------------------------------------------------------------------------------- /ui/dist/fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-regular.woff2 -------------------------------------------------------------------------------- /ui/dist/images/avatars/avatar0.svg: -------------------------------------------------------------------------------- 1 | Mary Roebling 2 | -------------------------------------------------------------------------------- /ui/dist/images/avatars/avatar1.svg: -------------------------------------------------------------------------------- 1 | Nellie Bly 2 | -------------------------------------------------------------------------------- /ui/dist/images/avatars/avatar2.svg: -------------------------------------------------------------------------------- 1 | Elizabeth Peratrovich 2 | -------------------------------------------------------------------------------- /ui/dist/images/avatars/avatar3.svg: -------------------------------------------------------------------------------- 1 | Amelia Boynton 2 | -------------------------------------------------------------------------------- /ui/dist/images/avatars/avatar4.svg: -------------------------------------------------------------------------------- 1 | Victoria Woodhull 2 | -------------------------------------------------------------------------------- /ui/dist/images/avatars/avatar5.svg: -------------------------------------------------------------------------------- 1 | Chien-Shiung 2 | -------------------------------------------------------------------------------- /ui/dist/images/avatars/avatar6.svg: -------------------------------------------------------------------------------- 1 | Hetty Green 2 | -------------------------------------------------------------------------------- /ui/dist/images/avatars/avatar7.svg: -------------------------------------------------------------------------------- 1 | Elizabeth Peratrovich 2 | -------------------------------------------------------------------------------- /ui/dist/images/avatars/avatar8.svg: -------------------------------------------------------------------------------- 1 | Jane Johnston 2 | -------------------------------------------------------------------------------- /ui/dist/images/avatars/avatar9.svg: -------------------------------------------------------------------------------- 1 | Virginia Apgar 2 | -------------------------------------------------------------------------------- /ui/dist/images/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/images/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /ui/dist/images/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/images/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /ui/dist/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /ui/dist/images/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/dist/images/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/images/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /ui/dist/images/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/images/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /ui/dist/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/images/favicon/favicon.ico -------------------------------------------------------------------------------- /ui/dist/images/favicon/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/images/favicon/mstile-144x144.png -------------------------------------------------------------------------------- /ui/dist/images/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/images/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /ui/dist/images/favicon/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/images/favicon/mstile-310x150.png -------------------------------------------------------------------------------- /ui/dist/images/favicon/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/images/favicon/mstile-310x310.png -------------------------------------------------------------------------------- /ui/dist/images/favicon/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/images/favicon/mstile-70x70.png -------------------------------------------------------------------------------- /ui/dist/images/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/_/images/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/_/images/favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/apple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/bitbucket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Facebook 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/gitee.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/livechat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/microsoft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Microsoft 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/oidc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/patreon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/strava.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/twitch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/vk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui/dist/images/oauth2/yandex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/plugins/code/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.8.4 (2024-06-19) 3 | */ 4 | !function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",(e=>((e=>{e.addCommand("mceCodeEditor",(()=>{(e=>{const o=(e=>e.getContent({source_view:!0}))(e);e.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:o},onSubmit:o=>{((e,o)=>{e.focus(),e.undoManager.transact((()=>{e.setContent(o)})),e.selection.setCursorLocation(),e.nodeChanged()})(e,o.getData().code),o.close()}})})(e)}))})(e),(e=>{const o=()=>e.execCommand("mceCodeEditor");e.ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:o}),e.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:o})})(e),{})))}(); -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/plugins/visualblocks/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.8.4 (2024-06-19) 3 | */ 4 | !function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const s=(t,s,o)=>{t.dom.toggleClass(t.getBody(),"mce-visualblocks"),o.set(!o.get()),((t,s)=>{t.dispatch("VisualBlocks",{state:s})})(t,o.get())},o=("visualblocks_default_state",t=>t.options.get("visualblocks_default_state"));const e=(t,s)=>o=>{o.setActive(s.get());const e=t=>o.setActive(t.state);return t.on("VisualBlocks",e),()=>t.off("VisualBlocks",e)};t.add("visualblocks",((t,l)=>{(t=>{(0,t.options.register)("visualblocks_default_state",{processor:"boolean",default:!1})})(t);const a=(t=>{let s=!1;return{get:()=>s,set:t=>{s=t}}})();((t,o,e)=>{t.addCommand("mceVisualBlocks",(()=>{s(t,0,e)}))})(t,0,a),((t,s)=>{const o=()=>t.execCommand("mceVisualBlocks");t.ui.registry.addToggleButton("visualblocks",{icon:"visualblocks",tooltip:"Show blocks",onAction:o,onSetup:e(t,s)}),t.ui.registry.addToggleMenuItem("visualblocks",{text:"Show blocks",icon:"visualblocks",onAction:o,onSetup:e(t,s)})})(t,a),((t,e,l)=>{t.on("PreviewFormats AfterPreviewFormats",(s=>{l.get()&&t.dom.toggleClass(t.getBody(),"mce-visualblocks","afterpreviewformats"===s.type)})),t.on("init",(()=>{o(t)&&s(t,0,l)}))})(t,0,a)}))}(); -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/skins/content/dark/content.min.css: -------------------------------------------------------------------------------- 1 | body{background-color:#222f3e;color:#fff;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}a{color:#4099ff}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#6d737b}figure{display:table;margin:1rem auto}figure figcaption{color:#8a8f97;display:block;margin-top:.25rem;text-align:center}hr{border-color:#6d737b;border-style:solid;border-width:1px 0 0 0}code{background-color:#6d737b;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #6d737b;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #6d737b;margin-right:1.5rem;padding-right:1rem} 2 | -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/skins/content/default/content.js: -------------------------------------------------------------------------------- 1 | tinymce.Resource.add('content/default/content.css', "body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\"0\"]):not([style*=border-width]) td,table[border]:not([border=\"0\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\"0\"]):not([style*=border-style]) td,table[border]:not([border=\"0\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\"0\"]):not([style*=border-color]) td,table[border]:not([border=\"0\"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}") 2 | //# sourceMappingURL=content.js.map 3 | -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/skins/content/default/content.min.css: -------------------------------------------------------------------------------- 1 | body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem} 2 | -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/skins/content/document/content.min.css: -------------------------------------------------------------------------------- 1 | @media screen{html{background:#f4f4f4;min-height:100%}}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif}@media screen{body{background-color:#fff;box-shadow:0 0 4px rgba(0,0,0,.15);box-sizing:border-box;margin:1rem auto 0;max-width:820px;min-height:calc(100vh - 1rem);padding:4rem 6rem 6rem 6rem}}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure figcaption{color:#999;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem} 2 | -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/skins/content/pocketbase/content.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 3 | line-height: 1.4; 4 | margin: 1rem; 5 | } 6 | table { 7 | border-collapse: collapse; 8 | } 9 | table th, 10 | table td { 11 | border: 1px solid #ccc; 12 | padding: 0.4rem; 13 | } 14 | figure { 15 | display: table; 16 | margin: 1rem auto; 17 | } 18 | figure figcaption { 19 | color: #999; 20 | display: block; 21 | margin-top: 0.25rem; 22 | text-align: center; 23 | } 24 | hr { 25 | border-color: #ccc; 26 | border-style: solid; 27 | border-width: 1px 0 0 0; 28 | } 29 | code { 30 | background-color: #e8e8e8; 31 | border-radius: 3px; 32 | padding: 0.1rem 0.2rem; 33 | } 34 | .mce-content-body:not([dir=rtl]) blockquote { 35 | border-left: 2px solid #ccc; 36 | margin-left: 1.5rem; 37 | padding-left: 1rem; 38 | } 39 | .mce-content-body[dir=rtl] blockquote { 40 | border-right: 2px solid #ccc; 41 | margin-right: 1.5rem; 42 | padding-right: 1rem; 43 | } 44 | -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/skins/content/pocketbase/content.min.css: -------------------------------------------------------------------------------- 1 | body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table td,table th{border:1px solid #ccc;padding:.4rem}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem} 2 | -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/skins/content/writer/content.min.css: -------------------------------------------------------------------------------- 1 | body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem auto;max-width:900px}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem} 2 | -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/skins/ui/oxide/skin.shadowdom.min.css: -------------------------------------------------------------------------------- 1 | body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201} 2 | -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/skins/ui/pocketbase/content.mobile.css: -------------------------------------------------------------------------------- 1 | .tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection { 2 | /* Note: this file is used inside the content, so isn't part of theming */ 3 | background-color: green; 4 | display: inline-block; 5 | opacity: 0.5; 6 | position: absolute; 7 | } 8 | body { 9 | -webkit-text-size-adjust: none; 10 | } 11 | body img { 12 | /* this is related to the content margin */ 13 | max-width: 96vw; 14 | } 15 | body table img { 16 | max-width: 95%; 17 | } 18 | body { 19 | font-family: sans-serif; 20 | } 21 | table { 22 | border-collapse: collapse; 23 | } 24 | -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/skins/ui/pocketbase/content.mobile.min.css: -------------------------------------------------------------------------------- 1 | .tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{background-color:green;display:inline-block;opacity:.5;position:absolute}body{-webkit-text-size-adjust:none}body img{max-width:96vw}body table img{max-width:95%}body{font-family:sans-serif}table{border-collapse:collapse} 2 | -------------------------------------------------------------------------------- /ui/dist/libs/tinymce/skins/ui/pocketbase/fonts/tinymce-mobile.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/dist/libs/tinymce/skins/ui/pocketbase/fonts/tinymce-mobile.woff -------------------------------------------------------------------------------- /ui/embed.go: -------------------------------------------------------------------------------- 1 | // Package ui handles the PocketBase Admin frontend embedding. 2 | package ui 3 | 4 | import ( 5 | "embed" 6 | 7 | "github.com/labstack/echo/v5" 8 | ) 9 | 10 | //go:embed all:dist 11 | var distDir embed.FS 12 | 13 | // DistDirFS contains the embedded dist directory files (without the "dist" prefix) 14 | var DistDirFS = echo.MustSubFS(distDir, "dist") 15 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pocketbase-admin", 3 | "private": true, 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview" 8 | }, 9 | "type": "module", 10 | "prettier": { 11 | "tabWidth": 4, 12 | "printWidth": 110, 13 | "svelteBracketNewLine": true 14 | }, 15 | "devDependencies": { 16 | "@codemirror/autocomplete": "^6.0.0", 17 | "@codemirror/commands": "^6.0.0", 18 | "@codemirror/lang-html": "^6.1.0", 19 | "@codemirror/lang-javascript": "^6.0.2", 20 | "@codemirror/lang-sql": "^6.4.0", 21 | "@codemirror/lang-json": "^6.0.0", 22 | "@codemirror/language": "^6.0.0", 23 | "@codemirror/legacy-modes": "^6.0.0", 24 | "@codemirror/search": "^6.0.0", 25 | "@codemirror/state": "^6.0.0", 26 | "@codemirror/view": "^6.0.0", 27 | "@sveltejs/vite-plugin-svelte": "^3.0.1", 28 | "chart.js": "^4.0.0", 29 | "chartjs-adapter-luxon": "^1.2.0", 30 | "luxon": "^3.4.4", 31 | "pocketbase": "^0.20.0", 32 | "sass": "^1.45.0", 33 | "svelte": "^4.0.0", 34 | "svelte-flatpickr": "^3.3.3", 35 | "svelte-spa-router": "^4.0.0", 36 | "vite": "^5.0.11" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ui/public/fonts/remixicon/remixicon.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/fonts/remixicon/remixicon.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-700.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-regular.woff2 -------------------------------------------------------------------------------- /ui/public/images/avatars/avatar0.svg: -------------------------------------------------------------------------------- 1 | Mary Roebling 2 | -------------------------------------------------------------------------------- /ui/public/images/avatars/avatar1.svg: -------------------------------------------------------------------------------- 1 | Nellie Bly 2 | -------------------------------------------------------------------------------- /ui/public/images/avatars/avatar2.svg: -------------------------------------------------------------------------------- 1 | Elizabeth Peratrovich 2 | -------------------------------------------------------------------------------- /ui/public/images/avatars/avatar3.svg: -------------------------------------------------------------------------------- 1 | Amelia Boynton 2 | -------------------------------------------------------------------------------- /ui/public/images/avatars/avatar4.svg: -------------------------------------------------------------------------------- 1 | Victoria Woodhull 2 | -------------------------------------------------------------------------------- /ui/public/images/avatars/avatar5.svg: -------------------------------------------------------------------------------- 1 | Chien-Shiung 2 | -------------------------------------------------------------------------------- /ui/public/images/avatars/avatar6.svg: -------------------------------------------------------------------------------- 1 | Hetty Green 2 | -------------------------------------------------------------------------------- /ui/public/images/avatars/avatar7.svg: -------------------------------------------------------------------------------- 1 | Elizabeth Peratrovich 2 | -------------------------------------------------------------------------------- /ui/public/images/avatars/avatar8.svg: -------------------------------------------------------------------------------- 1 | Jane Johnston 2 | -------------------------------------------------------------------------------- /ui/public/images/avatars/avatar9.svg: -------------------------------------------------------------------------------- 1 | Virginia Apgar 2 | -------------------------------------------------------------------------------- /ui/public/images/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/images/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /ui/public/images/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/images/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /ui/public/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /ui/public/images/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/public/images/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/images/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /ui/public/images/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/images/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /ui/public/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/images/favicon/favicon.ico -------------------------------------------------------------------------------- /ui/public/images/favicon/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/images/favicon/mstile-144x144.png -------------------------------------------------------------------------------- /ui/public/images/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/images/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /ui/public/images/favicon/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/images/favicon/mstile-310x150.png -------------------------------------------------------------------------------- /ui/public/images/favicon/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/images/favicon/mstile-310x310.png -------------------------------------------------------------------------------- /ui/public/images/favicon/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/images/favicon/mstile-70x70.png -------------------------------------------------------------------------------- /ui/public/images/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/_/images/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/_/images/favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/apple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/bitbucket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Facebook 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/gitee.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/livechat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/microsoft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Microsoft 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/oidc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/patreon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/strava.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/twitch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/vk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui/public/images/oauth2/yandex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui/public/libs/tinymce/license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ui/public/libs/tinymce/plugins/code/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.8.4 (2024-06-19) 3 | */ 4 | !function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",(e=>((e=>{e.addCommand("mceCodeEditor",(()=>{(e=>{const o=(e=>e.getContent({source_view:!0}))(e);e.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:o},onSubmit:o=>{((e,o)=>{e.focus(),e.undoManager.transact((()=>{e.setContent(o)})),e.selection.setCursorLocation(),e.nodeChanged()})(e,o.getData().code),o.close()}})})(e)}))})(e),(e=>{const o=()=>e.execCommand("mceCodeEditor");e.ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:o}),e.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:o})})(e),{})))}(); -------------------------------------------------------------------------------- /ui/public/libs/tinymce/plugins/visualblocks/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.8.4 (2024-06-19) 3 | */ 4 | !function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const s=(t,s,o)=>{t.dom.toggleClass(t.getBody(),"mce-visualblocks"),o.set(!o.get()),((t,s)=>{t.dispatch("VisualBlocks",{state:s})})(t,o.get())},o=("visualblocks_default_state",t=>t.options.get("visualblocks_default_state"));const e=(t,s)=>o=>{o.setActive(s.get());const e=t=>o.setActive(t.state);return t.on("VisualBlocks",e),()=>t.off("VisualBlocks",e)};t.add("visualblocks",((t,l)=>{(t=>{(0,t.options.register)("visualblocks_default_state",{processor:"boolean",default:!1})})(t);const a=(t=>{let s=!1;return{get:()=>s,set:t=>{s=t}}})();((t,o,e)=>{t.addCommand("mceVisualBlocks",(()=>{s(t,0,e)}))})(t,0,a),((t,s)=>{const o=()=>t.execCommand("mceVisualBlocks");t.ui.registry.addToggleButton("visualblocks",{icon:"visualblocks",tooltip:"Show blocks",onAction:o,onSetup:e(t,s)}),t.ui.registry.addToggleMenuItem("visualblocks",{text:"Show blocks",icon:"visualblocks",onAction:o,onSetup:e(t,s)})})(t,a),((t,e,l)=>{t.on("PreviewFormats AfterPreviewFormats",(s=>{l.get()&&t.dom.toggleClass(t.getBody(),"mce-visualblocks","afterpreviewformats"===s.type)})),t.on("init",(()=>{o(t)&&s(t,0,l)}))})(t,0,a)}))}(); -------------------------------------------------------------------------------- /ui/public/libs/tinymce/skins/content/dark/content.min.css: -------------------------------------------------------------------------------- 1 | body{background-color:#222f3e;color:#fff;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}a{color:#4099ff}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#6d737b}figure{display:table;margin:1rem auto}figure figcaption{color:#8a8f97;display:block;margin-top:.25rem;text-align:center}hr{border-color:#6d737b;border-style:solid;border-width:1px 0 0 0}code{background-color:#6d737b;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #6d737b;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #6d737b;margin-right:1.5rem;padding-right:1rem} 2 | -------------------------------------------------------------------------------- /ui/public/libs/tinymce/skins/content/default/content.js: -------------------------------------------------------------------------------- 1 | tinymce.Resource.add('content/default/content.css', "body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border=\"0\"]):not([style*=border-width]) td,table[border]:not([border=\"0\"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border=\"0\"]):not([style*=border-style]) td,table[border]:not([border=\"0\"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border=\"0\"]):not([style*=border-color]) td,table[border]:not([border=\"0\"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}") 2 | //# sourceMappingURL=content.js.map 3 | -------------------------------------------------------------------------------- /ui/public/libs/tinymce/skins/content/default/content.min.css: -------------------------------------------------------------------------------- 1 | body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem} 2 | -------------------------------------------------------------------------------- /ui/public/libs/tinymce/skins/content/document/content.min.css: -------------------------------------------------------------------------------- 1 | @media screen{html{background:#f4f4f4;min-height:100%}}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif}@media screen{body{background-color:#fff;box-shadow:0 0 4px rgba(0,0,0,.15);box-sizing:border-box;margin:1rem auto 0;max-width:820px;min-height:calc(100vh - 1rem);padding:4rem 6rem 6rem 6rem}}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure figcaption{color:#999;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem} 2 | -------------------------------------------------------------------------------- /ui/public/libs/tinymce/skins/content/pocketbase/content.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 3 | line-height: 1.4; 4 | margin: 1rem; 5 | } 6 | table { 7 | border-collapse: collapse; 8 | } 9 | table th, 10 | table td { 11 | border: 1px solid #ccc; 12 | padding: 0.4rem; 13 | } 14 | figure { 15 | display: table; 16 | margin: 1rem auto; 17 | } 18 | figure figcaption { 19 | color: #999; 20 | display: block; 21 | margin-top: 0.25rem; 22 | text-align: center; 23 | } 24 | hr { 25 | border-color: #ccc; 26 | border-style: solid; 27 | border-width: 1px 0 0 0; 28 | } 29 | code { 30 | background-color: #e8e8e8; 31 | border-radius: 3px; 32 | padding: 0.1rem 0.2rem; 33 | } 34 | .mce-content-body:not([dir=rtl]) blockquote { 35 | border-left: 2px solid #ccc; 36 | margin-left: 1.5rem; 37 | padding-left: 1rem; 38 | } 39 | .mce-content-body[dir=rtl] blockquote { 40 | border-right: 2px solid #ccc; 41 | margin-right: 1.5rem; 42 | padding-right: 1rem; 43 | } 44 | -------------------------------------------------------------------------------- /ui/public/libs/tinymce/skins/content/pocketbase/content.min.css: -------------------------------------------------------------------------------- 1 | body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table td,table th{border:1px solid #ccc;padding:.4rem}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem} 2 | -------------------------------------------------------------------------------- /ui/public/libs/tinymce/skins/content/writer/content.min.css: -------------------------------------------------------------------------------- 1 | body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem auto;max-width:900px}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem} 2 | -------------------------------------------------------------------------------- /ui/public/libs/tinymce/skins/ui/oxide/skin.shadowdom.min.css: -------------------------------------------------------------------------------- 1 | body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201} 2 | -------------------------------------------------------------------------------- /ui/public/libs/tinymce/skins/ui/pocketbase/content.mobile.css: -------------------------------------------------------------------------------- 1 | .tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection { 2 | /* Note: this file is used inside the content, so isn't part of theming */ 3 | background-color: green; 4 | display: inline-block; 5 | opacity: 0.5; 6 | position: absolute; 7 | } 8 | body { 9 | -webkit-text-size-adjust: none; 10 | } 11 | body img { 12 | /* this is related to the content margin */ 13 | max-width: 96vw; 14 | } 15 | body table img { 16 | max-width: 95%; 17 | } 18 | body { 19 | font-family: sans-serif; 20 | } 21 | table { 22 | border-collapse: collapse; 23 | } 24 | -------------------------------------------------------------------------------- /ui/public/libs/tinymce/skins/ui/pocketbase/content.mobile.min.css: -------------------------------------------------------------------------------- 1 | .tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{background-color:green;display:inline-block;opacity:.5;position:absolute}body{-webkit-text-size-adjust:none}body img{max-width:96vw}body table img{max-width:95%}body{font-family:sans-serif}table{border-collapse:collapse} 2 | -------------------------------------------------------------------------------- /ui/public/libs/tinymce/skins/ui/pocketbase/fonts/tinymce-mobile.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/rocketbase/8c950ad76724163db48da44f24c5fde8d0b734ff/ui/public/libs/tinymce/skins/ui/pocketbase/fonts/tinymce-mobile.woff -------------------------------------------------------------------------------- /ui/src/autocomplete.worker.js: -------------------------------------------------------------------------------- 1 | import CommonHelper from "@/utils/CommonHelper"; 2 | 3 | const maxKeys = 11000; 4 | 5 | onmessage = (e) => { 6 | if (!e.data.collections) { 7 | return; 8 | } 9 | 10 | const result = {}; 11 | 12 | result.baseKeys = CommonHelper.getCollectionAutocompleteKeys(e.data.collections, e.data.baseCollection?.name); 13 | result.baseKeys = limitArray(result.baseKeys.sort(keysSort), maxKeys); 14 | 15 | if (!e.data.disableRequestKeys) { 16 | result.requestKeys = CommonHelper.getRequestAutocompleteKeys(e.data.collections, e.data.baseCollection?.name); 17 | result.requestKeys = limitArray(result.requestKeys.sort(keysSort), maxKeys); 18 | } 19 | 20 | if (!e.data.disableCollectionJoinKeys) { 21 | result.collectionJoinKeys = CommonHelper.getCollectionJoinAutocompleteKeys(e.data.collections); 22 | result.collectionJoinKeys = limitArray(result.collectionJoinKeys.sort(keysSort), maxKeys); 23 | } 24 | 25 | postMessage(result); 26 | }; 27 | 28 | // sort shorter keys first 29 | function keysSort(a, b) { 30 | return a.length - b.length; 31 | } 32 | 33 | function limitArray(arr, max) { 34 | if (arr.length > max) { 35 | return arr.slice(0, max); 36 | } 37 | 38 | return arr; 39 | } 40 | -------------------------------------------------------------------------------- /ui/src/components/PageIndex.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | {#if showInstaller} 31 | 32 | { 34 | showInstaller = false; 35 | 36 | await tick(); 37 | 38 | // clear the installer param 39 | window.location.search = ""; 40 | }} 41 | /> 42 | 43 | {/if} 44 | -------------------------------------------------------------------------------- /ui/src/components/base/BaseSelectOption.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | {#if item.icon} 7 | 8 | {/if} 9 | 10 | {item.label || item.name || item.title || item.id || item.value} 11 | -------------------------------------------------------------------------------- /ui/src/components/base/CopyIcon.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 46 | -------------------------------------------------------------------------------- /ui/src/components/base/FormattedDate.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | {#if date} 21 |
22 |
{dateOnly}
23 |
{timeOnly} UTC
24 |
25 | {:else} 26 | N/A 27 | {/if} 28 | 29 | 41 | -------------------------------------------------------------------------------- /ui/src/components/base/FullPage.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 |
9 | {#if !nobranding} 10 |
11 | 20 |
21 |
22 | {/if} 23 | 24 | 25 |
26 | 27 | 28 | 33 | -------------------------------------------------------------------------------- /ui/src/components/base/InitialsAvatar.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | {CommonHelper.getInitials(value)} 12 | 13 |
14 | 15 | 30 | -------------------------------------------------------------------------------- /ui/src/components/base/MimeTypeSelectOption.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | {item.ext || "N/A"} 6 | {item.mimeType} 7 | -------------------------------------------------------------------------------- /ui/src/components/base/ModelDateIcon.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 39 | -------------------------------------------------------------------------------- /ui/src/components/base/MultipleValueInput.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | { 18 | value = CommonHelper.splitNonEmpty(e.target.value, separator); 19 | }} 20 | {...$$restProps} 21 | /> 22 | -------------------------------------------------------------------------------- /ui/src/components/base/PageSidebar.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | 24 | { 26 | initialSidebarWidth = sidebarElem.offsetWidth; 27 | }} 28 | on:dragging={(e) => { 29 | sidebarWidth = initialSidebarWidth + e.detail.diffX + "px"; 30 | }} 31 | on:dragstop={() => { 32 | CommonHelper.triggerResize(); 33 | }} 34 | /> 35 | -------------------------------------------------------------------------------- /ui/src/components/base/PageWrapper.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |
10 | 11 |
12 | 13 | 25 |
26 | -------------------------------------------------------------------------------- /ui/src/components/base/RedactedPasswordInput.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | {#if locked} 22 |
23 | 31 |
32 | 33 | 34 | {:else} 35 | 36 | {/if} 37 | -------------------------------------------------------------------------------- /ui/src/components/base/RefreshButton.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 | 44 | 45 | 55 | -------------------------------------------------------------------------------- /ui/src/components/base/SortHeader.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | toggleSort()} 31 | on:keydown={(e) => { 32 | if (e.code === "Enter" || e.code === "Space") { 33 | e.preventDefault(); 34 | toggleSort(); 35 | } 36 | }} 37 | > 38 | 39 | 40 | -------------------------------------------------------------------------------- /ui/src/components/base/UploadedFilePreview.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | {#if previewUrl} 30 | {file.name} 31 | {:else} 32 | 33 | {/if} 34 | -------------------------------------------------------------------------------- /ui/src/components/collections/docs/FieldsQueryParam.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | fields 9 | 10 | String 11 | 12 | 13 |

14 | Comma separated string of the fields to return in the JSON response 15 | (by default returns all fields). Ex.: 16 | 17 |

18 |

19 | * targets all keys from the specific depth level. 20 |

21 |

In addition, the following field modifiers are also supported:

22 |
    23 |
  • 24 | :excerpt(maxLength, withEllipsis?) 25 |
    26 | Returns a short plain text version of the field string value. 27 |
    28 | Ex.: 29 | ?fields=*,{prefix}description:excerpt(200,true) 30 |
  • 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /ui/src/components/collections/schema/SchemaFieldBool.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ui/src/components/collections/schema/SchemaFieldEditor.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 24 | 25 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /ui/src/components/collections/schema/SchemaFieldUrl.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/src/components/logs/LogDate.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | {date.replace("Z", " UTC")} 18 | 19 | -------------------------------------------------------------------------------- /ui/src/components/logs/LogLevel.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 | {label || "UNKN"} ({level}) 12 | 13 |
14 | 15 | 41 | -------------------------------------------------------------------------------- /ui/src/components/logs/LogsLevelsInfo.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | Default log levels: 10 |
11 | {#each logLevels as options} 12 | {options.level}:{options.label} 13 | {/each} 14 |
15 |
16 | -------------------------------------------------------------------------------- /ui/src/components/records/PageOAuth2Redirect.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |

Auth completed.

11 |
You can close this window and go back to the app.
12 |
13 | -------------------------------------------------------------------------------- /ui/src/components/records/PageOAuth2RedirectFailure.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |

Auth failed.

11 |
You can close this window and go back to the app to try again.
12 |
13 | -------------------------------------------------------------------------------- /ui/src/components/records/PageOAuth2RedirectSuccess.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |

Auth completed.

11 |
You can close this window and go back to the app.
12 |
13 | -------------------------------------------------------------------------------- /ui/src/components/records/fields/BoolField.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ui/src/components/records/fields/EmailField.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ui/src/components/records/fields/NumberField.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 14 | 23 | 24 | -------------------------------------------------------------------------------- /ui/src/components/records/fields/SelectField.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 25 | 15 | 16 | -------------------------------------------------------------------------------- /ui/src/components/settings/TokenField.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | { 20 | // toggle 21 | if (secret) { 22 | secret = undefined; 23 | } else { 24 | secret = CommonHelper.randomSecret(50); 25 | } 26 | }} 27 | > 28 | Invalidate all previously issued tokens 29 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /ui/src/components/settings/providers/AppleOptions.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | { 22 | config.clientSecret = e.detail?.secret || ""; 23 | }} 24 | /> 25 | -------------------------------------------------------------------------------- /ui/src/components/settings/providers/MicrosoftOptions.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
Azure AD endpoints
9 | 10 | 11 | 12 |
13 | Eg. {`https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/authorize`} 14 |
15 |
16 | 17 | 18 | 19 |
20 | Eg. {`https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/token`} 21 |
22 |
23 | -------------------------------------------------------------------------------- /ui/src/components/settings/providers/SelfHostedOptions.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
{title}
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ui/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte' 2 | 3 | const app = new App({ 4 | target: document.getElementById('app') 5 | }); 6 | 7 | export default app 8 | -------------------------------------------------------------------------------- /ui/src/scss/_bulkbar.scss: -------------------------------------------------------------------------------- 1 | .bulkbar { 2 | position: absolute; 3 | bottom: var(--baseSpacing); 4 | left: 50%; 5 | z-index: 101; 6 | gap: 10px; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | width: var(--smWrapperWidth); 11 | max-width: 100%; 12 | margin-bottom: 10px; 13 | padding: 10px var(--smSpacing); 14 | border-radius: var(--btnHeight); 15 | background: var(--baseColor); 16 | border: 1px solid var(--baseAlt2Color); 17 | @include shadowize(); 18 | transform: translateX(-50%); 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/scss/_reset.scss: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | body { 27 | line-height: 1; 28 | } 29 | ol, ul { 30 | list-style: none; 31 | } 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { 37 | content: ''; 38 | content: none; 39 | } 40 | table { 41 | border-collapse: collapse; 42 | border-spacing: 0; 43 | } 44 | 45 | html { 46 | box-sizing: border-box; 47 | } 48 | *, *:before, *:after { 49 | box-sizing: inherit; 50 | } 51 | -------------------------------------------------------------------------------- /ui/src/scss/_tooltip.scss: -------------------------------------------------------------------------------- 1 | .app-tooltip { 2 | position: fixed; 3 | z-index: 999999; 4 | top: 0; 5 | left: 0; 6 | display: inline-block; 7 | vertical-align: top; 8 | max-width: 275px; 9 | padding: 3px 5px; 10 | color: #fff; 11 | text-align: center; 12 | font-family: var(--baseFontFamily); 13 | font-size: var(--smFontSize); 14 | line-height: var(--smLineHeight); 15 | border-radius: var(--baseRadius); 16 | background: var(--tooltipColor); 17 | pointer-events: none; 18 | user-select: none; 19 | transition: opacity var(--baseAnimationSpeed), 20 | visibility var(--baseAnimationSpeed), 21 | transform var(--baseAnimationSpeed); 22 | transform: translateY(1px); 23 | backface-visibility: hidden; 24 | white-space: pre-line; 25 | word-break: break-word; 26 | @include hide(); 27 | 28 | // styles 29 | &.code { 30 | font-family: monospace; 31 | white-space: pre-wrap; 32 | text-align: left; 33 | min-width: 150px; 34 | max-width: 340px; 35 | } 36 | 37 | // states 38 | &.active { 39 | transform: scale(1); 40 | @include show(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/src/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import 'fonts'; 2 | 3 | @import 'vars'; 4 | 5 | @import 'mixins'; 6 | 7 | @import 'reset'; 8 | 9 | @import 'icons'; 10 | 11 | @import 'animations'; 12 | 13 | @import 'base'; 14 | 15 | @import 'grid'; 16 | 17 | @import 'tooltip'; 18 | 19 | @import 'dropdown'; 20 | 21 | @import 'overlay_panel'; 22 | 23 | @import 'alert'; 24 | 25 | @import 'form'; 26 | 27 | @import 'layout'; 28 | 29 | @import 'tabs'; 30 | 31 | @import 'accordion'; 32 | 33 | @import 'table'; 34 | 35 | @import 'searchbar'; 36 | 37 | @import 'bulkbar'; 38 | 39 | @import 'flatpickr'; 40 | 41 | @import 'docs_panel'; 42 | 43 | @import 'schema_field'; 44 | 45 | @import 'file_picker'; 46 | 47 | @import 'collections_export'; 48 | -------------------------------------------------------------------------------- /ui/src/stores/admin.js: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | // logged app admin 4 | export const admin = writable({}); 5 | 6 | export function setAdmin(model) { 7 | admin.set(model || {}); 8 | } 9 | -------------------------------------------------------------------------------- /ui/src/stores/app.js: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | export const pageTitle = writable(''); 4 | 5 | export const appName = writable(''); 6 | 7 | export const hideControls = writable(false); 8 | -------------------------------------------------------------------------------- /ui/src/stores/confirmation.js: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | // eg. 4 | // { 5 | // "text": "Do you really want to delete the selectedItem", 6 | // "yesCallback": function() {...}, 7 | // "noCallback": function() {...}, 8 | // } 9 | export const confirmation = writable({}); 10 | 11 | /** 12 | * @param {String} text 13 | * @param {Function} [yesCallback] 14 | * @param {Function} [noCallback] 15 | */ 16 | export function confirm(text, yesCallback, noCallback) { 17 | confirmation.set({ 18 | text: text, 19 | yesCallback: yesCallback, 20 | noCallback: noCallback, 21 | }); 22 | } 23 | 24 | export function resetConfirmation() { 25 | confirmation.set({}); 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/stores/errors.js: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | import CommonHelper from "@/utils/CommonHelper"; 3 | 4 | export const errors = writable({}); 5 | 6 | /** 7 | * @param {Object} newErrors 8 | */ 9 | export function setErrors(newErrors) { 10 | errors.set(newErrors || {}); 11 | } 12 | 13 | /** 14 | * @param {String} name 15 | * @param {String|Array} message 16 | */ 17 | export function addError(name, message) { 18 | errors.update((e) => { 19 | CommonHelper.setByPath(e, name, CommonHelper.sentenize(message)) 20 | return e; 21 | }); 22 | } 23 | 24 | /** 25 | * @param {String} name 26 | */ 27 | export function removeError(name) { 28 | errors.update((e) => { 29 | CommonHelper.deleteByPath(e, name); 30 | return e; 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /ui/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | // see https://vitejs.dev/config 5 | export default defineConfig({ 6 | server: { 7 | port: 3000, 8 | }, 9 | envPrefix: 'PB', 10 | base: './', 11 | build: { 12 | chunkSizeWarningLimit: 1000, 13 | reportCompressedSize: false, 14 | }, 15 | plugins: [ 16 | svelte({ 17 | preprocess: [vitePreprocess()], 18 | onwarn: (warning, handler) => { 19 | if (warning.code.startsWith('a11y-')) { 20 | return; // silence a11y warnings 21 | } 22 | handler(warning); 23 | }, 24 | }), 25 | ], 26 | resolve: { 27 | alias: { 28 | '@': __dirname + '/src', 29 | } 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /volumes/.gitignore: -------------------------------------------------------------------------------- 1 | ./* 2 | !.gitignore 3 | --------------------------------------------------------------------------------