├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── js-sdk.yml │ ├── lint.yml │ ├── main.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .mockery.yaml ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── Makefile ├── README.md ├── billing ├── checkout │ ├── checkout.go │ └── service.go ├── config.go ├── credit │ ├── credit.go │ ├── mocks │ │ └── transaction_repository.go │ ├── service.go │ └── service_test.go ├── customer │ ├── customer.go │ ├── customer_test.go │ ├── errors.go │ ├── mocks │ │ ├── credit_service.go │ │ └── repository.go │ ├── service.go │ └── service_test.go ├── entitlement │ ├── error.go │ ├── mocks │ │ ├── organization_service.go │ │ ├── plan_service.go │ │ ├── product_service.go │ │ └── subscription_service.go │ ├── service.go │ └── service_test.go ├── invoice │ ├── invoice.go │ ├── service.go │ └── service_test.go ├── plan │ ├── plan.go │ └── service.go ├── product │ ├── errors.go │ ├── filter.go │ ├── mocks │ │ ├── feature_repository.go │ │ ├── price_repository.go │ │ └── repository.go │ ├── product.go │ ├── service.go │ └── service_test.go ├── stripetest │ ├── backend.go │ └── mocks │ │ └── backend.go ├── subscription │ ├── mocks │ │ ├── credit_service.go │ │ ├── customer_service.go │ │ ├── organization_service.go │ │ ├── plan_service.go │ │ ├── product_service.go │ │ └── repository.go │ ├── service.go │ ├── service_test.go │ └── subscription.go └── usage │ ├── service.go │ └── usage.go ├── buf.gen.yaml ├── cmd ├── client.go ├── config.go ├── context.go ├── errors.go ├── group.go ├── group_test.go ├── help.go ├── migrate.go ├── namespace.go ├── namespace_test.go ├── organization.go ├── organization_test.go ├── permission.go ├── permission_test.go ├── policy.go ├── policy_test.go ├── preferences.go ├── project.go ├── project_test.go ├── role.go ├── role_test.go ├── root.go ├── seed.go ├── seed │ ├── organizations.json │ ├── permissions.json │ ├── projects.json │ ├── resource.json │ ├── roles.json │ └── users.json ├── serve.go ├── server.go ├── user.go └── version.go ├── config ├── build.go ├── config.go ├── config_test.go ├── init.go ├── sample.config.yaml └── testdata │ └── use_duration.yaml ├── core ├── aggregates │ ├── orgbilling │ │ └── service.go │ ├── orginvoices │ │ └── service.go │ ├── orgprojects │ │ └── service.go │ ├── orgserviceusercredentials │ │ └── service.go │ ├── orgtokens │ │ └── service.go │ ├── orgusers │ │ └── service.go │ ├── projectusers │ │ └── service.go │ ├── userorgs │ │ └── service.go │ └── userprojects │ │ └── service.go ├── audit │ ├── audit.go │ ├── context.go │ ├── filter.go │ ├── io_repository.go │ ├── logger.go │ ├── noop_repository.go │ ├── service.go │ └── service_test.go ├── authenticate │ ├── authenticate.go │ ├── config.go │ ├── context.go │ ├── errors.go │ ├── mocks │ │ ├── flow_repository.go │ │ ├── service_user_service.go │ │ ├── session_service.go │ │ ├── token_service.go │ │ └── user_service.go │ ├── service.go │ ├── service_test.go │ ├── session │ │ ├── mocks │ │ │ └── repository.go │ │ ├── service.go │ │ ├── service_test.go │ │ └── session.go │ ├── strategy │ │ ├── mail_link.go │ │ ├── mail_otp.go │ │ ├── mail_otp_test.go │ │ ├── oidc.go │ │ └── passkey.go │ ├── test_users │ │ └── config.go │ └── token │ │ └── service.go ├── deleter │ ├── deleter.go │ └── service.go ├── domain │ ├── domain.go │ ├── errors.go │ ├── filter.go │ └── service.go ├── event │ ├── event.go │ ├── listener.go │ ├── mocks │ │ ├── checkout_service.go │ │ ├── credit_service.go │ │ ├── customer_service.go │ │ ├── invoice_service.go │ │ ├── organization_service.go │ │ ├── plan_service.go │ │ ├── subscription_service.go │ │ └── user_service.go │ ├── publisher.go │ ├── service.go │ └── service_test.go ├── group │ ├── errors.go │ ├── filter.go │ ├── group.go │ ├── mocks │ │ ├── authn_service.go │ │ ├── policy_service.go │ │ ├── relation_service.go │ │ └── repository.go │ ├── service.go │ └── service_test.go ├── invitation │ ├── config.go │ ├── filter.go │ ├── invite.go │ ├── mocks │ │ ├── group_service.go │ │ ├── organization_service.go │ │ ├── policy_service.go │ │ ├── preferences_service.go │ │ ├── relation_service.go │ │ ├── repository.go │ │ └── user_service.go │ ├── service.go │ └── service_test.go ├── kyc │ ├── errors.go │ ├── kyc.go │ ├── mocks │ │ └── repository.go │ ├── service.go │ └── service_test.go ├── metaschema │ ├── errors.go │ ├── metaschema.go │ └── service.go ├── namespace │ ├── errors.go │ ├── namespace.go │ └── service.go ├── organization │ ├── errors.go │ ├── filter.go │ ├── mocks │ │ ├── authn_service.go │ │ ├── policy_service.go │ │ ├── preferences_service.go │ │ ├── relation_service.go │ │ ├── repository.go │ │ └── user_service.go │ ├── organization.go │ ├── service.go │ └── service_test.go ├── permission │ ├── errors.go │ ├── filter.go │ ├── mocks │ │ └── repository.go │ ├── permission.go │ ├── service.go │ └── service_test.go ├── policy │ ├── errors.go │ ├── filter.go │ ├── mocks │ │ ├── relation_service.go │ │ ├── repository.go │ │ └── role_service.go │ ├── policy.go │ ├── service.go │ └── service_test.go ├── preference │ ├── filter.go │ ├── preference.go │ ├── service.go │ └── validator.go ├── project │ ├── errors.go │ ├── filter.go │ ├── mocks │ │ ├── authn_service.go │ │ ├── group_service.go │ │ ├── policy_service.go │ │ ├── relation_service.go │ │ ├── repository.go │ │ ├── serviceuser_service.go │ │ └── user_service.go │ ├── project.go │ ├── service.go │ └── service_test.go ├── prospect │ ├── errors.go │ ├── mocks │ │ └── repository.go │ ├── prospect.go │ ├── service.go │ └── service_test.go ├── relation │ ├── errors.go │ ├── relation.go │ └── service.go ├── resource │ ├── errors.go │ ├── filter.go │ ├── resource.go │ └── service.go ├── role │ ├── errors.go │ ├── filter.go │ ├── mocks │ │ ├── permission_service.go │ │ ├── relation_service.go │ │ └── repository.go │ ├── role.go │ ├── service.go │ └── service_test.go ├── serviceuser │ ├── errors.go │ ├── filter.go │ ├── service.go │ └── serviceuser.go ├── user │ ├── errors.go │ ├── filter.go │ ├── mocks │ │ ├── policy_service.go │ │ ├── relation_service.go │ │ ├── repository.go │ │ └── role_service.go │ ├── service.go │ ├── service_test.go │ └── user.go └── webhook │ ├── config.go │ ├── errors.go │ ├── service.go │ └── webhook.go ├── docker-compose.yml ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── Frontier_23June2024_HighLevel.png │ ├── admin-portal.md │ ├── admin-settings.md │ ├── apis │ │ ├── admin-service-add-platform-user.api.mdx │ │ ├── admin-service-admin-create-organization.api.mdx │ │ ├── admin-service-check-federated-resource-permission.api.mdx │ │ ├── admin-service-create-permission.api.mdx │ │ ├── admin-service-create-preferences.api.mdx │ │ ├── admin-service-create-prospect.api.mdx │ │ ├── admin-service-create-role.api.mdx │ │ ├── admin-service-create-webhook.api.mdx │ │ ├── admin-service-delegated-checkout-2.api.mdx │ │ ├── admin-service-delegated-checkout.api.mdx │ │ ├── admin-service-delete-permission.api.mdx │ │ ├── admin-service-delete-prospect.api.mdx │ │ ├── admin-service-delete-role.api.mdx │ │ ├── admin-service-delete-webhook.api.mdx │ │ ├── admin-service-generate-invoices.api.mdx │ │ ├── admin-service-get-billing-account-details.api.mdx │ │ ├── admin-service-get-prospect.api.mdx │ │ ├── admin-service-list-all-billing-accounts.api.mdx │ │ ├── admin-service-list-all-invoices.api.mdx │ │ ├── admin-service-list-all-organizations.api.mdx │ │ ├── admin-service-list-all-users.api.mdx │ │ ├── admin-service-list-groups.api.mdx │ │ ├── admin-service-list-organizations-kyc.api.mdx │ │ ├── admin-service-list-platform-users.api.mdx │ │ ├── admin-service-list-preferences.api.mdx │ │ ├── admin-service-list-projects.api.mdx │ │ ├── admin-service-list-prospects.api.mdx │ │ ├── admin-service-list-relations.api.mdx │ │ ├── admin-service-list-resources.api.mdx │ │ ├── admin-service-list-webhooks.api.mdx │ │ ├── admin-service-remove-platform-user.api.mdx │ │ ├── admin-service-revert-billing-usage-2.api.mdx │ │ ├── admin-service-revert-billing-usage.api.mdx │ │ ├── admin-service-search-organization-invoices.api.mdx │ │ ├── admin-service-search-organization-projects.api.mdx │ │ ├── admin-service-search-organization-service-user-credentials.api.mdx │ │ ├── admin-service-search-organization-tokens.api.mdx │ │ ├── admin-service-search-organization-users.api.mdx │ │ ├── admin-service-search-organizations.api.mdx │ │ ├── admin-service-search-project-users.api.mdx │ │ ├── admin-service-search-user-organizations.api.mdx │ │ ├── admin-service-search-user-projects.api.mdx │ │ ├── admin-service-search-users.api.mdx │ │ ├── admin-service-set-organization-kyc.api.mdx │ │ ├── admin-service-update-billing-account-details.api.mdx │ │ ├── admin-service-update-billing-account-limits.api.mdx │ │ ├── admin-service-update-permission.api.mdx │ │ ├── admin-service-update-prospect.api.mdx │ │ ├── admin-service-update-role.api.mdx │ │ ├── admin-service-update-webhook.api.mdx │ │ ├── frontier-administration-api.info.mdx │ │ ├── frontier-service-accept-organization-invitation.api.mdx │ │ ├── frontier-service-add-group-users.api.mdx │ │ ├── frontier-service-add-organization-users.api.mdx │ │ ├── frontier-service-auth-callback-2.api.mdx │ │ ├── frontier-service-auth-callback.api.mdx │ │ ├── frontier-service-auth-logout-2.api.mdx │ │ ├── frontier-service-auth-logout.api.mdx │ │ ├── frontier-service-auth-token.api.mdx │ │ ├── frontier-service-authenticate-2.api.mdx │ │ ├── frontier-service-authenticate.api.mdx │ │ ├── frontier-service-batch-check-permission.api.mdx │ │ ├── frontier-service-billing-webhook-callback.api.mdx │ │ ├── frontier-service-cancel-subscription-2.api.mdx │ │ ├── frontier-service-cancel-subscription.api.mdx │ │ ├── frontier-service-change-subscription-2.api.mdx │ │ ├── frontier-service-change-subscription.api.mdx │ │ ├── frontier-service-check-feature-entitlement-2.api.mdx │ │ ├── frontier-service-check-feature-entitlement.api.mdx │ │ ├── frontier-service-check-resource-permission.api.mdx │ │ ├── frontier-service-create-billing-account.api.mdx │ │ ├── frontier-service-create-billing-usage-2.api.mdx │ │ ├── frontier-service-create-billing-usage.api.mdx │ │ ├── frontier-service-create-checkout-2.api.mdx │ │ ├── frontier-service-create-checkout.api.mdx │ │ ├── frontier-service-create-current-user-preferences.api.mdx │ │ ├── frontier-service-create-feature.api.mdx │ │ ├── frontier-service-create-group-preferences.api.mdx │ │ ├── frontier-service-create-group.api.mdx │ │ ├── frontier-service-create-meta-schema.api.mdx │ │ ├── frontier-service-create-organization-audit-logs.api.mdx │ │ ├── frontier-service-create-organization-domain.api.mdx │ │ ├── frontier-service-create-organization-invitation.api.mdx │ │ ├── frontier-service-create-organization-preferences.api.mdx │ │ ├── frontier-service-create-organization-role.api.mdx │ │ ├── frontier-service-create-organization.api.mdx │ │ ├── frontier-service-create-plan.api.mdx │ │ ├── frontier-service-create-policy-for-project.api.mdx │ │ ├── frontier-service-create-policy.api.mdx │ │ ├── frontier-service-create-product.api.mdx │ │ ├── frontier-service-create-project-preferences.api.mdx │ │ ├── frontier-service-create-project-resource.api.mdx │ │ ├── frontier-service-create-project.api.mdx │ │ ├── frontier-service-create-prospect-public.api.mdx │ │ ├── frontier-service-create-relation.api.mdx │ │ ├── frontier-service-create-service-user-credential.api.mdx │ │ ├── frontier-service-create-service-user-jwk.api.mdx │ │ ├── frontier-service-create-service-user-token.api.mdx │ │ ├── frontier-service-create-service-user.api.mdx │ │ ├── frontier-service-create-user-preferences.api.mdx │ │ ├── frontier-service-create-user.api.mdx │ │ ├── frontier-service-delete-billing-account.api.mdx │ │ ├── frontier-service-delete-group.api.mdx │ │ ├── frontier-service-delete-meta-schema.api.mdx │ │ ├── frontier-service-delete-organization-domain.api.mdx │ │ ├── frontier-service-delete-organization-invitation.api.mdx │ │ ├── frontier-service-delete-organization-role.api.mdx │ │ ├── frontier-service-delete-organization.api.mdx │ │ ├── frontier-service-delete-policy.api.mdx │ │ ├── frontier-service-delete-project-resource.api.mdx │ │ ├── frontier-service-delete-project.api.mdx │ │ ├── frontier-service-delete-relation.api.mdx │ │ ├── frontier-service-delete-service-user-credential.api.mdx │ │ ├── frontier-service-delete-service-user-jwk.api.mdx │ │ ├── frontier-service-delete-service-user-token.api.mdx │ │ ├── frontier-service-delete-service-user.api.mdx │ │ ├── frontier-service-delete-user.api.mdx │ │ ├── frontier-service-describe-preferences.api.mdx │ │ ├── frontier-service-disable-billing-account.api.mdx │ │ ├── frontier-service-disable-group.api.mdx │ │ ├── frontier-service-disable-organization.api.mdx │ │ ├── frontier-service-disable-project.api.mdx │ │ ├── frontier-service-disable-user.api.mdx │ │ ├── frontier-service-enable-billing-account.api.mdx │ │ ├── frontier-service-enable-group.api.mdx │ │ ├── frontier-service-enable-organization.api.mdx │ │ ├── frontier-service-enable-project.api.mdx │ │ ├── frontier-service-enable-user.api.mdx │ │ ├── frontier-service-get-billing-account.api.mdx │ │ ├── frontier-service-get-billing-balance.api.mdx │ │ ├── frontier-service-get-checkout-2.api.mdx │ │ ├── frontier-service-get-checkout.api.mdx │ │ ├── frontier-service-get-current-user.api.mdx │ │ ├── frontier-service-get-feature.api.mdx │ │ ├── frontier-service-get-group.api.mdx │ │ ├── frontier-service-get-jw-ks-2.api.mdx │ │ ├── frontier-service-get-jw-ks.api.mdx │ │ ├── frontier-service-get-meta-schema.api.mdx │ │ ├── frontier-service-get-namespace.api.mdx │ │ ├── frontier-service-get-organization-audit-log.api.mdx │ │ ├── frontier-service-get-organization-domain.api.mdx │ │ ├── frontier-service-get-organization-invitation.api.mdx │ │ ├── frontier-service-get-organization-kyc.api.mdx │ │ ├── frontier-service-get-organization-role.api.mdx │ │ ├── frontier-service-get-organization.api.mdx │ │ ├── frontier-service-get-permission.api.mdx │ │ ├── frontier-service-get-plan.api.mdx │ │ ├── frontier-service-get-policy.api.mdx │ │ ├── frontier-service-get-product.api.mdx │ │ ├── frontier-service-get-project-resource.api.mdx │ │ ├── frontier-service-get-project.api.mdx │ │ ├── frontier-service-get-relation.api.mdx │ │ ├── frontier-service-get-service-user-jwk.api.mdx │ │ ├── frontier-service-get-service-user.api.mdx │ │ ├── frontier-service-get-subscription-2.api.mdx │ │ ├── frontier-service-get-subscription.api.mdx │ │ ├── frontier-service-get-upcoming-invoice-2.api.mdx │ │ ├── frontier-service-get-upcoming-invoice.api.mdx │ │ ├── frontier-service-get-user.api.mdx │ │ ├── frontier-service-has-trialed.api.mdx │ │ ├── frontier-service-join-organization.api.mdx │ │ ├── frontier-service-list-auth-strategies.api.mdx │ │ ├── frontier-service-list-billing-accounts.api.mdx │ │ ├── frontier-service-list-billing-transactions-2.api.mdx │ │ ├── frontier-service-list-billing-transactions.api.mdx │ │ ├── frontier-service-list-checkouts-2.api.mdx │ │ ├── frontier-service-list-checkouts.api.mdx │ │ ├── frontier-service-list-current-user-groups.api.mdx │ │ ├── frontier-service-list-current-user-invitations.api.mdx │ │ ├── frontier-service-list-current-user-preferences.api.mdx │ │ ├── frontier-service-list-features.api.mdx │ │ ├── frontier-service-list-group-preferences.api.mdx │ │ ├── frontier-service-list-group-users.api.mdx │ │ ├── frontier-service-list-invoices-2.api.mdx │ │ ├── frontier-service-list-invoices.api.mdx │ │ ├── frontier-service-list-meta-schemas.api.mdx │ │ ├── frontier-service-list-namespaces.api.mdx │ │ ├── frontier-service-list-organization-admins.api.mdx │ │ ├── frontier-service-list-organization-audit-logs.api.mdx │ │ ├── frontier-service-list-organization-domains.api.mdx │ │ ├── frontier-service-list-organization-groups.api.mdx │ │ ├── frontier-service-list-organization-invitations.api.mdx │ │ ├── frontier-service-list-organization-preferences.api.mdx │ │ ├── frontier-service-list-organization-projects.api.mdx │ │ ├── frontier-service-list-organization-roles.api.mdx │ │ ├── frontier-service-list-organization-service-users.api.mdx │ │ ├── frontier-service-list-organization-users.api.mdx │ │ ├── frontier-service-list-organizations-by-current-user.api.mdx │ │ ├── frontier-service-list-organizations-by-user.api.mdx │ │ ├── frontier-service-list-organizations.api.mdx │ │ ├── frontier-service-list-permissions.api.mdx │ │ ├── frontier-service-list-plans.api.mdx │ │ ├── frontier-service-list-policies.api.mdx │ │ ├── frontier-service-list-products.api.mdx │ │ ├── frontier-service-list-project-admins.api.mdx │ │ ├── frontier-service-list-project-groups.api.mdx │ │ ├── frontier-service-list-project-preferences.api.mdx │ │ ├── frontier-service-list-project-resources.api.mdx │ │ ├── frontier-service-list-project-service-users.api.mdx │ │ ├── frontier-service-list-project-users.api.mdx │ │ ├── frontier-service-list-projects-by-current-user.api.mdx │ │ ├── frontier-service-list-projects-by-user.api.mdx │ │ ├── frontier-service-list-roles.api.mdx │ │ ├── frontier-service-list-service-user-credentials.api.mdx │ │ ├── frontier-service-list-service-user-jw-ks.api.mdx │ │ ├── frontier-service-list-service-user-projects.api.mdx │ │ ├── frontier-service-list-service-user-tokens.api.mdx │ │ ├── frontier-service-list-service-users.api.mdx │ │ ├── frontier-service-list-subscriptions-2.api.mdx │ │ ├── frontier-service-list-subscriptions.api.mdx │ │ ├── frontier-service-list-user-groups.api.mdx │ │ ├── frontier-service-list-user-invitations.api.mdx │ │ ├── frontier-service-list-user-preferences.api.mdx │ │ ├── frontier-service-list-users.api.mdx │ │ ├── frontier-service-register-billing-account.api.mdx │ │ ├── frontier-service-remove-group-user.api.mdx │ │ ├── frontier-service-remove-organization-user.api.mdx │ │ ├── frontier-service-total-debited-transactions.api.mdx │ │ ├── frontier-service-update-billing-account.api.mdx │ │ ├── frontier-service-update-current-user.api.mdx │ │ ├── frontier-service-update-feature.api.mdx │ │ ├── frontier-service-update-group.api.mdx │ │ ├── frontier-service-update-meta-schema.api.mdx │ │ ├── frontier-service-update-organization-role.api.mdx │ │ ├── frontier-service-update-organization.api.mdx │ │ ├── frontier-service-update-plan.api.mdx │ │ ├── frontier-service-update-policy.api.mdx │ │ ├── frontier-service-update-product.api.mdx │ │ ├── frontier-service-update-project-resource.api.mdx │ │ ├── frontier-service-update-project.api.mdx │ │ ├── frontier-service-update-subscription-2.api.mdx │ │ ├── frontier-service-update-subscription.api.mdx │ │ ├── frontier-service-update-user.api.mdx │ │ ├── frontier-service-verify-organization-domain.api.mdx │ │ └── sidebar.js │ ├── authn │ │ ├── introduction.md │ │ ├── org-domain.md │ │ ├── serviceuser.md │ │ ├── user.md │ │ ├── user_auth_google_oidc.png │ │ ├── user_auth_session_accesstoken.png │ │ ├── user_auth_session_proxy.png │ │ └── user_auth_supported_strategy.png │ ├── authz │ │ ├── example.md │ │ ├── overview.md │ │ ├── permission.md │ │ ├── policy.md │ │ └── role.md │ ├── basics.md │ ├── billing │ │ ├── billing_customers.md │ │ ├── billing_subscriptions.md │ │ └── introduction.md │ ├── concepts │ │ ├── architecture.md │ │ ├── frontier-authorization-architecture.png │ │ ├── frontier-proxy-architecture.png │ │ ├── glossary.md │ │ └── overall-proxy-architecture.png │ ├── configurations.md │ ├── contribution │ │ ├── contribute.md │ │ └── development.md │ ├── frontier-flow-diagram.png │ ├── installation.md │ ├── introduction.md │ ├── prospects │ │ └── introduction.md │ ├── reference │ │ ├── api-auth.md │ │ ├── api-definitions.md │ │ ├── billing-configurations.md │ │ ├── cli.md │ │ ├── configurations.md │ │ ├── metaschemas.md │ │ ├── shell-autocomplete.md │ │ ├── smtp.md │ │ └── webhook.md │ ├── support.md │ ├── tenants │ │ ├── managing-resource.md │ │ ├── org.md │ │ └── project.md │ ├── tour │ │ ├── authn-1.png │ │ ├── authn-2.png │ │ ├── authn-3.png │ │ ├── authn-4.png │ │ ├── creating-user.md │ │ ├── intro.md │ │ ├── permission-schema-after.png │ │ ├── permission-schema-before.png │ │ ├── server-start-cmd-output.png │ │ ├── setup-idp-oidc.md │ │ ├── shield-connection-config.png │ │ ├── shield-tables.png │ │ ├── spicedb-connection-config.png │ │ └── spicedb-tables.png │ ├── users │ │ ├── group.md │ │ └── principal.md │ └── vercel.json ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src │ ├── core │ │ ├── Container.js │ │ └── GridBlock.js │ ├── css │ │ ├── custom.css │ │ ├── icons.css │ │ └── theme.css │ └── theme │ │ └── Mermaid.js ├── static │ ├── .nojekyll │ └── img │ │ ├── architecture.svg │ │ ├── overview.svg │ │ └── shield.svg └── yarn.lock ├── examples └── authn │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── static │ └── index.html ├── go.mod ├── go.sum ├── internal ├── api │ ├── api.go │ └── v1beta1 │ │ ├── audit.go │ │ ├── audit_test.go │ │ ├── authenticate.go │ │ ├── authenticate_test.go │ │ ├── billing_check.go │ │ ├── billing_checkout.go │ │ ├── billing_customer.go │ │ ├── billing_customer_test.go │ │ ├── billing_invoice.go │ │ ├── billing_plan.go │ │ ├── billing_product.go │ │ ├── billing_subscription.go │ │ ├── billing_usage.go │ │ ├── billing_webhook.go │ │ ├── deleter.go │ │ ├── deleter_test.go │ │ ├── domain.go │ │ ├── domain_test.go │ │ ├── errors.go │ │ ├── group.go │ │ ├── group_test.go │ │ ├── invitations.go │ │ ├── invitations_test.go │ │ ├── kyc.go │ │ ├── kyc_test.go │ │ ├── metaschema.go │ │ ├── metaschema_test.go │ │ ├── mocks │ │ ├── audit_service.go │ │ ├── authn_service.go │ │ ├── bootstrap_service.go │ │ ├── cascade_deleter.go │ │ ├── checkout_service.go │ │ ├── credit_service.go │ │ ├── customer_service.go │ │ ├── domain_service.go │ │ ├── entitlement_service.go │ │ ├── event_service.go │ │ ├── group_service.go │ │ ├── invitation_service.go │ │ ├── invoice_service.go │ │ ├── kyc_service.go │ │ ├── meta_schema_service.go │ │ ├── namespace_service.go │ │ ├── org_billing_service.go │ │ ├── org_invoices_service.go │ │ ├── org_projects_service.go │ │ ├── org_service_user_credentials_service.go │ │ ├── org_tokens_service.go │ │ ├── org_users_service.go │ │ ├── organization_service.go │ │ ├── permission_service.go │ │ ├── plan_service.go │ │ ├── policy_service.go │ │ ├── preference_service.go │ │ ├── product_service.go │ │ ├── project_service.go │ │ ├── project_users_service.go │ │ ├── prospect_service.go │ │ ├── relation_service.go │ │ ├── resource_service.go │ │ ├── role_service.go │ │ ├── service_user_service.go │ │ ├── session_service.go │ │ ├── subscription_service.go │ │ ├── usage_service.go │ │ ├── user_orgs_service.go │ │ ├── user_service.go │ │ └── webhook_service.go │ │ ├── namespace.go │ │ ├── namespace_test.go │ │ ├── org.go │ │ ├── org_invoices.go │ │ ├── org_projects.go │ │ ├── org_serviceuser_credentials.go │ │ ├── org_test.go │ │ ├── org_tokens.go │ │ ├── org_users.go │ │ ├── orgbilling.go │ │ ├── permission.go │ │ ├── permission_check.go │ │ ├── permission_check_test.go │ │ ├── permission_test.go │ │ ├── platform.go │ │ ├── policy.go │ │ ├── policy_test.go │ │ ├── preferences.go │ │ ├── preferences_test.go │ │ ├── project.go │ │ ├── project_test.go │ │ ├── project_users.go │ │ ├── prospect.go │ │ ├── prospect_test.go │ │ ├── relation.go │ │ ├── relation_test.go │ │ ├── resource.go │ │ ├── resource_test.go │ │ ├── role.go │ │ ├── role_test.go │ │ ├── serviceuser.go │ │ ├── serviceuser_test.go │ │ ├── user.go │ │ ├── user_orgs.go │ │ ├── user_projects.go │ │ ├── user_test.go │ │ ├── v1beta1.go │ │ └── webhook.go ├── bootstrap │ ├── generator.go │ ├── generator_test.go │ ├── schema │ │ ├── base_schema.zed │ │ ├── schema.go │ │ └── schema_test.go │ ├── service.go │ └── testdata │ │ ├── compiled_schema.zed │ │ └── compute_service.yml ├── metrics │ ├── db.go │ ├── metrics.go │ ├── service.go │ └── stripe.go └── store │ ├── blob │ ├── blob.go │ ├── plan_repository.go │ ├── plans │ │ └── sample.yaml │ ├── resources_repository.go │ ├── schema_repository.go │ ├── schema_repository_test.go │ └── testdata │ │ ├── compute.yml │ │ └── database.yml │ ├── postgres │ ├── audit.go │ ├── audit_repository.go │ ├── billing_checkout_repository.go │ ├── billing_customer_repository.go │ ├── billing_customer_repository_test.go │ ├── billing_feature_repository.go │ ├── billing_invoice_repository.go │ ├── billing_plan_repository.go │ ├── billing_price_repository.go │ ├── billing_product_repository.go │ ├── billing_product_repository_test.go │ ├── billing_subscription_repository.go │ ├── billing_transactions_repository.go │ ├── billing_transactions_repository_test.go │ ├── domain.go │ ├── domain_repository.go │ ├── errors.go │ ├── flow.go │ ├── flow_repository.go │ ├── group.go │ ├── group_repository.go │ ├── group_repository_test.go │ ├── invitation.go │ ├── invitation_repository.go │ ├── invitation_repository_test.go │ ├── kyc.go │ ├── kyc_repository.go │ ├── kyc_repository_test.go │ ├── lock_test.go │ ├── metaschema.go │ ├── metaschema_repository.go │ ├── metaschemas │ │ ├── group.json │ │ ├── org.json │ │ ├── prospect.json │ │ ├── role.json │ │ └── user.json │ ├── migrations │ │ ├── 20230507191406_create_base_tables.down.sql │ │ ├── 20230507191406_create_base_tables.up.sql │ │ ├── 20230731211912_create_domain_table.down.sql │ │ ├── 20230731211912_create_domain_table.up.sql │ │ ├── 20230814084723_add_role_title.down.sql │ │ ├── 20230814084723_add_role_title.up.sql │ │ ├── 20230819134146_create_preferences_table.down.sql │ │ ├── 20230819134146_create_preferences_table.up.sql │ │ ├── 20230907010521_add_users_orgs_avatar.down.sql │ │ ├── 20230907010521_add_users_orgs_avatar.up.sql │ │ ├── 20230923183419_add_scopes_in_roles.down.sql │ │ ├── 20230923183419_add_scopes_in_roles.up.sql │ │ ├── 20231012175641_create_billing_tables.down.sql │ │ ├── 20231012175641_create_billing_tables.up.sql │ │ ├── 20231231021142_add_feature_behavior.down.sql │ │ ├── 20231231021142_add_feature_behavior.up.sql │ │ ├── 20240106162115_refactor_features.down.sql │ │ ├── 20240106162115_refactor_features.up.sql │ │ ├── 20240118131733_product_config.down.sql │ │ ├── 20240118131733_product_config.up.sql │ │ ├── 20240124202702_subscription_changes.down.sql │ │ ├── 20240124202702_subscription_changes.up.sql │ │ ├── 20240206233616_plan_trial_days.down.sql │ │ ├── 20240206233616_plan_trial_days.up.sql │ │ ├── 20240212105822_invoice_table.down.sql │ │ ├── 20240212105822_invoice_table.up.sql │ │ ├── 20240301162743_checkout_subscription_config.down.sql │ │ ├── 20240301162743_checkout_subscription_config.up.sql │ │ ├── 20240307201705_feature_title.down.sql │ │ ├── 20240307201705_feature_title.up.sql │ │ ├── 20240312181615_transactions_user_id.down.sql │ │ ├── 20240312181615_transactions_user_id.up.sql │ │ ├── 20240409114211_billing_customers_tax.down.sql │ │ ├── 20240409114211_billing_customers_tax.up.sql │ │ ├── 20240413083012_serviceuser_credential_type.down.sql │ │ ├── 20240413083012_serviceuser_credential_type.up.sql │ │ ├── 20240427034606_webhook_endpoint_table.down.sql │ │ ├── 20240427034606_webhook_endpoint_table.up.sql │ │ ├── 20240502075306_billing_customer_provider_notnull.down.sql │ │ ├── 20240502075306_billing_customer_provider_notnull.up.sql │ │ ├── 20240917183506_billing_customer_credit_min.down.sql │ │ ├── 20240917183506_billing_customer_credit_min.up.sql │ │ ├── 20241015201506_billing_invoice_items.down.sql │ │ ├── 20241015201506_billing_invoice_items.up.sql │ │ ├── 20250220054302_org_kyc.down.sql │ │ ├── 20250220054302_org_kyc.up.sql │ │ ├── 20250303090030_create_prospect_table.down.sql │ │ ├── 20250303090030_create_prospect_table.up.sql │ │ ├── 20250410211534_billing_customer_due_in_days.down.sql │ │ ├── 20250410211534_billing_customer_due_in_days.up.sql │ │ ├── 20250528135454_billing_customer_due_in_days_default_30.down.sql │ │ ├── 20250528135454_billing_customer_due_in_days_default_30.up.sql │ │ └── migrations.go │ ├── namespace.go │ ├── namespace_repository.go │ ├── namespace_repository_test.go │ ├── org_billing_repository.go │ ├── org_billing_repository_test.go │ ├── org_invoices_repository.go │ ├── org_invoices_repository_test.go │ ├── org_projects_repository.go │ ├── org_projects_repository_test.go │ ├── org_serviceuser_credentials_repository.go │ ├── org_serviceuser_credentials_repository_test.go │ ├── org_tokens_repository.go │ ├── org_tokens_repository_test.go │ ├── org_users_repository.go │ ├── org_users_repository_test.go │ ├── organization.go │ ├── organization_repository.go │ ├── organization_repository_test.go │ ├── permission.go │ ├── permission_repository.go │ ├── permission_repository_test.go │ ├── policy.go │ ├── policy_repository.go │ ├── policy_repository_test.go │ ├── postgres.go │ ├── postgres_test.go │ ├── preference.go │ ├── preference_repository.go │ ├── project.go │ ├── project_repository.go │ ├── project_repository_test.go │ ├── project_users_repository.go │ ├── project_users_repository_test.go │ ├── prospect.go │ ├── prospect_repository.go │ ├── prospect_repository_test.go │ ├── relation.go │ ├── relation_repository.go │ ├── relation_repository_test.go │ ├── resource.go │ ├── resource_repository.go │ ├── resource_repository_test.go │ ├── role.go │ ├── role_repository.go │ ├── role_repository_test.go │ ├── serviceuser.go │ ├── serviceuser_credential_repository.go │ ├── serviceuser_repository.go │ ├── session.go │ ├── session_repository.go │ ├── testdata │ │ ├── mock-group.json │ │ ├── mock-invitation.json │ │ ├── mock-namespace.json │ │ ├── mock-organization-kyc.json │ │ ├── mock-organization.json │ │ ├── mock-permission.json │ │ ├── mock-policy.json │ │ ├── mock-project.json │ │ ├── mock-prospect.json │ │ ├── mock-relation.json │ │ ├── mock-resource.json │ │ ├── mock-role.json │ │ └── mock-user.json │ ├── user.go │ ├── user_orgs_repository.go │ ├── user_orgs_repository_test.go │ ├── user_projects_repository.go │ ├── user_projects_repository_test.go │ ├── user_repository.go │ ├── user_repository_test.go │ ├── webhook_endpoint.go │ ├── webhook_endpoint_repository.go │ └── webhook_endpoint_repository_test.go │ └── spicedb │ ├── config.go │ ├── relation_repository.go │ ├── schema_repository.go │ └── spicedb.go ├── main.go ├── pkg ├── crypt │ ├── aes.go │ ├── hmac.go │ ├── key.go │ └── random.go ├── db │ ├── config.go │ └── db.go ├── debounce │ ├── debounce.go │ └── debounce_test.go ├── errors │ └── errors.go ├── file │ └── file.go ├── httputil │ ├── context.go │ └── header.go ├── logger │ ├── config.go │ └── logger.go ├── mailer │ ├── dialer.go │ ├── mailer.go │ └── mocks │ │ └── dialer.go ├── metadata │ └── metadata.go ├── pagination │ └── pagination.go ├── server │ ├── config.go │ ├── consts │ │ └── gateway.go │ ├── health │ │ ├── health.go │ │ └── health_test.go │ ├── interceptors │ │ ├── audit.go │ │ ├── authentication.go │ │ ├── authorization.go │ │ ├── byte_mimetype.go │ │ ├── cors.go │ │ ├── enrich.go │ │ ├── errors.go │ │ ├── errors_test.go │ │ ├── headers.go │ │ ├── passthrough_header.go │ │ └── session.go │ └── server.go ├── str │ ├── slug.go │ └── utils.go ├── utils │ ├── domain.go │ ├── jwk.go │ ├── jwk_test.go │ ├── pointers.go │ ├── rql.go │ ├── slice.go │ ├── slice_test.go │ ├── time.go │ └── uuid.go └── webhook │ └── validate.go ├── proto ├── apidocs.swagger.yaml └── v1beta1 │ ├── admin.pb.go │ ├── admin.pb.gw.go │ ├── admin.pb.validate.go │ ├── admin_grpc.pb.go │ ├── frontier.pb.go │ ├── frontier.pb.gw.go │ ├── frontier.pb.validate.go │ ├── frontier_grpc.pb.go │ ├── models.pb.go │ └── models.pb.validate.go ├── sdks └── js │ ├── .changeset │ ├── README.md │ ├── config.json │ └── cool-terms-smell.md │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── package.json │ ├── packages │ ├── core │ │ ├── .release-it.json │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── api-client │ │ │ ├── V1Beta1.ts │ │ │ ├── WellKnown.ts │ │ │ ├── data-contracts.ts │ │ │ ├── http-client.ts │ │ │ └── index.ts │ │ ├── custom.d.ts │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── react │ │ │ ├── assets │ │ │ │ ├── bell-slash.svg │ │ │ │ ├── bell.svg │ │ │ │ ├── check-circle.svg │ │ │ │ ├── chevron-left.svg │ │ │ │ ├── close-close.svg │ │ │ │ ├── close-default.svg │ │ │ │ ├── close.svg │ │ │ │ ├── coin.svg │ │ │ │ ├── cross.svg │ │ │ │ ├── exclamation-triangle.svg │ │ │ │ ├── key.svg │ │ │ │ ├── line.svg │ │ │ │ ├── logo.png │ │ │ │ ├── logos │ │ │ │ │ └── google-logo.svg │ │ │ │ ├── open.svg │ │ │ │ ├── organization.png │ │ │ │ ├── resize-collapse.svg │ │ │ │ ├── resize-default.svg │ │ │ │ ├── resize-expand.svg │ │ │ │ ├── user.png │ │ │ │ └── users.svg │ │ │ ├── components │ │ │ │ ├── Container.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── Layout │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── layout.module.css │ │ │ │ ├── avatar-upload │ │ │ │ │ ├── avatar-upload.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── common │ │ │ │ │ └── upcoming-plan-change-banner │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── styles.module.css │ │ │ │ ├── container.module.css │ │ │ │ ├── header.module.css │ │ │ │ ├── helpers │ │ │ │ │ └── Amount │ │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── onboarding │ │ │ │ │ ├── magiclink-verify.tsx │ │ │ │ │ ├── magiclink.tsx │ │ │ │ │ ├── oidc.tsx │ │ │ │ │ ├── onboarding.module.css │ │ │ │ │ ├── signin.tsx │ │ │ │ │ ├── signup.tsx │ │ │ │ │ ├── subscribe.tsx │ │ │ │ │ └── updates.tsx │ │ │ │ ├── organization │ │ │ │ │ ├── api-keys │ │ │ │ │ │ ├── add.tsx │ │ │ │ │ │ ├── columns.tsx │ │ │ │ │ │ ├── delete.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── service-user │ │ │ │ │ │ │ ├── add-token.tsx │ │ │ │ │ │ │ ├── delete.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── projects.tsx │ │ │ │ │ │ │ └── styles.module.css │ │ │ │ │ │ └── styles.module.css │ │ │ │ │ ├── billing │ │ │ │ │ │ ├── billing.module.css │ │ │ │ │ │ ├── cycle-switch │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── invoices │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── invoice.module.css │ │ │ │ │ │ ├── payment-issue.tsx │ │ │ │ │ │ ├── payment-method.tsx │ │ │ │ │ │ └── upcoming-billing-cycle.tsx │ │ │ │ │ ├── create.tsx │ │ │ │ │ ├── domain │ │ │ │ │ │ ├── add-domain.tsx │ │ │ │ │ │ ├── delete.tsx │ │ │ │ │ │ ├── domain.columns.tsx │ │ │ │ │ │ ├── domain.module.css │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── verify-domain.tsx │ │ │ │ │ ├── general │ │ │ │ │ │ ├── delete.tsx │ │ │ │ │ │ ├── general.module.css │ │ │ │ │ │ ├── general.workspace.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── members │ │ │ │ │ │ ├── MemberRemoveConfirm.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── invite.tsx │ │ │ │ │ │ ├── member.columns.tsx │ │ │ │ │ │ ├── member.types.tsx │ │ │ │ │ │ └── members.module.css │ │ │ │ │ ├── organization.module.css │ │ │ │ │ ├── plans │ │ │ │ │ │ ├── confirm-change │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── header.tsx │ │ │ │ │ │ ├── helpers │ │ │ │ │ │ │ ├── helpers.test.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── hooks │ │ │ │ │ │ │ └── usePlans.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── plans.module.css │ │ │ │ │ │ └── pricing-column.tsx │ │ │ │ │ ├── preferences │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── preferences.types.ts │ │ │ │ │ ├── profile.tsx │ │ │ │ │ ├── project │ │ │ │ │ │ ├── add.tsx │ │ │ │ │ │ ├── delete.tsx │ │ │ │ │ │ ├── general │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── members │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── member.columns.tsx │ │ │ │ │ │ │ ├── members.module.css │ │ │ │ │ │ │ └── remove.tsx │ │ │ │ │ │ ├── project.module.css │ │ │ │ │ │ ├── project.tsx │ │ │ │ │ │ └── projects.columns.tsx │ │ │ │ │ ├── routes.tsx │ │ │ │ │ ├── security │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── security.types.tsx │ │ │ │ │ ├── sidebar │ │ │ │ │ │ ├── helpers.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── sidebar.module.css │ │ │ │ │ ├── styles.ts │ │ │ │ │ ├── teams │ │ │ │ │ │ ├── add.tsx │ │ │ │ │ │ ├── delete.tsx │ │ │ │ │ │ ├── general │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── members │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── invite.tsx │ │ │ │ │ │ │ ├── member.columns.tsx │ │ │ │ │ │ │ └── members.module.css │ │ │ │ │ │ ├── team.tsx │ │ │ │ │ │ ├── teams.columns.tsx │ │ │ │ │ │ └── teams.module.css │ │ │ │ │ ├── tokens │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── token.module.css │ │ │ │ │ │ └── transactions │ │ │ │ │ │ │ ├── columns.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── user │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── update.tsx │ │ │ │ └── window │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── window.module.css │ │ │ ├── contexts │ │ │ │ ├── FrontierContext.tsx │ │ │ │ ├── FrontierProvider.tsx │ │ │ │ └── useMaxAllowedInstancesGuard.tsx │ │ │ ├── hooks │ │ │ │ ├── useBillingPermission.ts │ │ │ │ ├── useCopyToClipboard.ts │ │ │ │ ├── useOrganizationDomains.ts │ │ │ │ ├── useOrganizationMembers.ts │ │ │ │ ├── useOrganizationProjects.ts │ │ │ │ ├── useOrganizationTeams.ts │ │ │ │ ├── usePermissions.ts │ │ │ │ ├── usePreferences.ts │ │ │ │ └── useTokens.ts │ │ │ ├── index.ts │ │ │ ├── package.json │ │ │ └── utils │ │ │ │ ├── constants.ts │ │ │ │ └── index.ts │ │ ├── scripts │ │ │ └── bump-version.js │ │ ├── shared │ │ │ └── types.ts │ │ ├── src │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── tsconfig.json │ │ ├── tsup.config.ts │ │ └── utils │ │ │ └── index.ts │ └── sdk-demo │ │ ├── .dockerignore │ │ ├── .eslintignore │ │ ├── .eslintrc.js │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── next.config.mjs │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── src │ │ ├── api │ │ │ └── frontier.ts │ │ ├── app │ │ │ ├── callback │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── login │ │ │ │ └── page.tsx │ │ │ ├── magiclink-verify │ │ │ │ └── page.tsx │ │ │ ├── organizations │ │ │ │ └── [orgId] │ │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── signup │ │ │ │ └── page.tsx │ │ │ ├── subscribe │ │ │ │ └── page.tsx │ │ │ └── updates │ │ │ │ └── page.tsx │ │ ├── config │ │ │ └── frontier.ts │ │ ├── contexts │ │ │ └── auth │ │ │ │ ├── index.tsx │ │ │ │ └── provider.tsx │ │ ├── hooks │ │ │ └── useAuthRedirect.tsx │ │ ├── pages │ │ │ └── api │ │ │ │ └── [...all].ts │ │ └── utils │ │ │ └── custom-fetch.ts │ │ └── tsconfig.json │ ├── pnpm-lock.yaml │ ├── pnpm-workspace.yaml │ ├── tools │ ├── eslint-config │ │ ├── index.js │ │ └── package.json │ └── tsconfig │ │ ├── base.json │ │ ├── nextjs.json │ │ ├── node14.json │ │ ├── package.json │ │ └── react-library.json │ └── turbo.json ├── test └── e2e │ ├── regression │ ├── api_test.go │ ├── authentication_test.go │ ├── billing_test.go │ ├── onboarding_test.go │ ├── passthrough_header_test.go │ ├── service_registration_test.go │ ├── serviceusers_test.go │ └── testdata │ │ ├── .gitignore │ │ ├── cassettes │ │ └── .gitkeep │ │ ├── jwks.json │ │ ├── plans │ │ └── subscription.credits.yaml │ │ └── resource │ │ └── compute.yml │ ├── smoke │ ├── ping_test.go │ └── testdata │ │ └── resource │ │ └── potato.yaml │ └── testbench │ ├── echo.go │ ├── frontier.go │ ├── helper.go │ ├── postgres.go │ ├── spicedb.go │ ├── stripe.go │ ├── testbench.go │ └── testdata │ └── mocks │ ├── mock-group.json │ ├── mock-organization.json │ ├── mock-project.json │ └── mock-user.json └── ui ├── .env.example ├── .eslintrc.cjs ├── .gitignore ├── Makefile ├── README.md ├── dist └── .gitkeep ├── embed.go ├── index.html ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public ├── dashboard.svg ├── groups.svg ├── logo.svg ├── organisations.svg ├── product.svg ├── projects.svg ├── share.svg └── users.svg ├── scripts └── gen-swagger-client.mjs ├── src ├── App.css ├── App.tsx ├── api │ ├── frontier.ts │ └── index.ts ├── assets │ ├── data │ │ └── countries.json │ └── icons │ │ ├── admins.svg │ │ ├── coin-colored.svg │ │ ├── coin.svg │ │ ├── cpu-chip.svg │ │ ├── iam.svg │ │ ├── invoices.svg │ │ ├── plans.svg │ │ ├── preferences.svg │ │ ├── products.svg │ │ ├── roles.svg │ │ ├── users.svg │ │ └── webhooks.svg ├── components │ ├── CustomField.tsx │ ├── DialogTable.tsx │ ├── Layout.tsx │ ├── Price.tsx │ ├── Sidebar │ │ ├── index.tsx │ │ └── sidebar.module.css │ ├── TableLoader │ │ └── index.tsx │ ├── assign-role.tsx │ ├── collapsable-search │ │ └── index.tsx │ ├── dialog │ │ └── header.tsx │ ├── multiselect │ │ └── index.tsx │ ├── page-header.module.css │ ├── page-header.tsx │ ├── page-title │ │ └── index.tsx │ ├── sheet │ │ ├── footer.tsx │ │ └── header.tsx │ └── states │ │ ├── Loading.tsx │ │ └── Unauthorized.tsx ├── configs │ ├── frontier.tsx │ └── theme.tsx ├── containers │ ├── billingplans.list │ │ ├── columns.tsx │ │ ├── details.tsx │ │ ├── header.tsx │ │ └── index.tsx │ ├── groups.create │ │ └── index.tsx │ ├── groups.list │ │ ├── columns.tsx │ │ ├── details.tsx │ │ ├── header.tsx │ │ ├── index.tsx │ │ └── types.tsx │ ├── home.tsx │ ├── invoices.list │ │ ├── columns.tsx │ │ ├── header.tsx │ │ └── index.tsx │ ├── login.tsx │ ├── magiclink.tsx │ ├── preferences.list │ │ ├── columns.tsx │ │ ├── details.tsx │ │ ├── index.tsx │ │ └── layout.tsx │ ├── products.create │ │ ├── base-fields.tsx │ │ ├── contants.ts │ │ ├── features-fields.tsx │ │ ├── index.tsx │ │ ├── metadata-fields.tsx │ │ ├── price-fields.tsx │ │ └── transform.ts │ ├── products.edit │ │ └── index.tsx │ ├── products.list │ │ ├── columns.tsx │ │ ├── details.tsx │ │ ├── header.tsx │ │ ├── index.tsx │ │ ├── prices │ │ │ ├── columns.tsx │ │ │ └── index.tsx │ │ └── types.tsx │ ├── roles.list │ │ ├── columns.tsx │ │ ├── details.tsx │ │ ├── header.tsx │ │ ├── index.tsx │ │ └── types.tsx │ ├── super_admins │ │ ├── columns.tsx │ │ └── list.tsx │ ├── users.create │ │ └── index.tsx │ ├── users.list │ │ ├── columns.tsx │ │ ├── details.tsx │ │ ├── header.tsx │ │ ├── index.tsx │ │ ├── styles.module.css │ │ └── types.tsx │ └── webhooks │ │ ├── columns.tsx │ │ ├── create │ │ └── index.tsx │ │ ├── header.tsx │ │ ├── index.tsx │ │ └── update │ │ └── index.tsx ├── contexts │ └── App.tsx ├── hooks │ ├── useFeatures.ts │ └── useRQL.tsx ├── layout │ └── auth.tsx ├── main.tsx ├── pages │ ├── organizations │ │ ├── details │ │ │ ├── apis │ │ │ │ ├── apis.module.css │ │ │ │ ├── columns.tsx │ │ │ │ └── index.tsx │ │ │ ├── contexts │ │ │ │ └── organization-context.tsx │ │ │ ├── edit │ │ │ │ ├── billing.tsx │ │ │ │ ├── edit.module.css │ │ │ │ ├── kyc.tsx │ │ │ │ └── organization.tsx │ │ │ ├── index.tsx │ │ │ ├── invoices │ │ │ │ ├── columns.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── invoices.module.css │ │ │ ├── layout │ │ │ │ ├── add-tokens-dialog.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── invite-users-dialog.tsx │ │ │ │ ├── layout.module.css │ │ │ │ └── navbar.tsx │ │ │ ├── members │ │ │ │ ├── columns.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── members.module.css │ │ │ │ └── remove-member.tsx │ │ │ ├── projects │ │ │ │ ├── columns.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── members │ │ │ │ │ ├── add-members-dropdown.tsx │ │ │ │ │ ├── assign-role.tsx │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── members.module.css │ │ │ │ │ └── remove-member.tsx │ │ │ │ ├── projects.module.css │ │ │ │ ├── rename-project.tsx │ │ │ │ └── useAddProjectMembers.tsx │ │ │ ├── security │ │ │ │ ├── block-organization.tsx │ │ │ │ ├── domains-list.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── security.module.css │ │ │ ├── side-panel │ │ │ │ ├── billing-details-section.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── kyc-section.tsx │ │ │ │ ├── org-details-section.tsx │ │ │ │ ├── plan-details-section.tsx │ │ │ │ ├── side-panel.module.css │ │ │ │ └── tokens-details-section.tsx │ │ │ ├── tokens │ │ │ │ ├── columns.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── tokens.module.css │ │ │ └── types.ts │ │ └── list │ │ │ ├── columns.tsx │ │ │ ├── create.tsx │ │ │ ├── index.tsx │ │ │ ├── list.module.css │ │ │ └── navbar.tsx │ └── users │ │ ├── details │ │ ├── audit-log │ │ │ ├── audit-log.module.css │ │ │ ├── audit-log.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── layout │ │ │ ├── index.ts │ │ │ ├── layout.module.css │ │ │ ├── layout.tsx │ │ │ ├── membership-dropdown.tsx │ │ │ ├── navbar.module.css │ │ │ ├── navbar.tsx │ │ │ ├── side-panel-details.tsx │ │ │ ├── side-panel-membership.tsx │ │ │ ├── side-panel.module.css │ │ │ ├── side-panel.tsx │ │ │ └── suspend-user.tsx │ │ ├── security │ │ │ ├── block-user.tsx │ │ │ ├── index.ts │ │ │ ├── security.module.css │ │ │ └── security.tsx │ │ ├── user-context.tsx │ │ └── user-details.tsx │ │ ├── list │ │ ├── columns.tsx │ │ ├── index.ts │ │ ├── invite-users.module.css │ │ ├── invite-users.tsx │ │ ├── list.module.css │ │ ├── list.tsx │ │ └── navbar.tsx │ │ └── util.ts ├── routes.tsx ├── styles.ts ├── utils │ ├── constants.ts │ ├── helper.ts │ └── webhook_events.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── types ├── HttpResponse.tsx └── types.d.ts └── vite.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | README.md 4 | LICENSE 5 | .vscode 6 | test 7 | .idea/ 8 | temp/ 9 | vendor/ 10 | config.yml 11 | coverage.out 12 | resource_config 13 | ui/dist/ui 14 | ui/node_modules/ 15 | ui/.env 16 | dist/ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | dev: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - name: Set up Go 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: "1.22.5" 19 | - name: Login to DockerHub 20 | uses: docker/login-action@v3 21 | with: 22 | registry: docker.io 23 | username: ${{ secrets.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | - name: Publish dev image 26 | id: docker_dev_build 27 | uses: docker/build-push-action@v2 28 | with: 29 | push: true 30 | file: "./Dockerfile.dev" 31 | tags: raystack/frontier:dev 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .env 3 | .DS_Store 4 | .idea/ 5 | .vscode/ 6 | proxies/*.yaml 7 | proxies/*.yml 8 | .temp 9 | temp 10 | frontier 11 | config.yaml 12 | config.yml 13 | coverage.txt 14 | coverage.out 15 | *.pprof 16 | ignore/ 17 | vendor/ 18 | buf.lock 19 | buf.yaml 20 | /resources_config 21 | resource_config 22 | /rules 23 | /dist 24 | __debug_* 25 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | skip-dirs: 4 | - ui 5 | output: 6 | format: colored-line-number 7 | linters: 8 | enable-all: false 9 | disable-all: true 10 | enable: 11 | - vet 12 | - goimports 13 | - thelper 14 | - unconvert 15 | - revive 16 | - unused 17 | - gofmt 18 | - whitespace 19 | - misspell 20 | - govet 21 | - importas 22 | - protogetter 23 | - ineffassign 24 | - wastedassign 25 | # - gosec 26 | # - prealloc 27 | # - errcheck 28 | # - errorlint 29 | # - staticcheck 30 | # - gosimple 31 | # - gocritic 32 | # - goconst 33 | # - perfsprint 34 | linters-settings: 35 | revive: 36 | ignore-generated-header: true 37 | severity: warning 38 | severity: 39 | default-severity: error -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.18 2 | 3 | COPY frontier /usr/bin/frontier 4 | 5 | ENTRYPOINT ["frontier"] -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # Build the UI 2 | FROM node:18-alpine3.18 as ui-builder 3 | RUN apk add --no-cache make 4 | WORKDIR /app 5 | 6 | COPY ui ./ui 7 | COPY Makefile . 8 | RUN make ui 9 | 10 | # Build the Go binary 11 | FROM golang:1.22.3-alpine3.18 as builder 12 | RUN apk add --no-cache make 13 | WORKDIR /app 14 | 15 | COPY go.mod go.sum ./ 16 | RUN go mod download 17 | 18 | COPY . . 19 | COPY --from=ui-builder /app/ui/dist /app/ui/dist 20 | 21 | RUN make build 22 | 23 | # Build the final image 24 | FROM alpine:3.18 25 | COPY --from=builder /app/frontier /usr/bin/ 26 | RUN apk update && \ 27 | apk add --no-cache ca-certificates libc6-compat && \ 28 | rm -rf /var/cache/apk/* 29 | 30 | ENTRYPOINT ["frontier"] -------------------------------------------------------------------------------- /billing/customer/errors.go: -------------------------------------------------------------------------------- 1 | package customer 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrNotFound = errors.New("customer not found") 9 | ErrInvalidUUID = errors.New("invalid syntax of uuid") 10 | ErrInvalidID = errors.New("billing customer id is invalid") 11 | ErrConflict = errors.New("customer already exist") 12 | ErrActiveConflict = errors.New("an active account already exists for the organization") 13 | ErrInvalidDetail = errors.New("invalid billing customer detail") 14 | ErrDisabled = errors.New("billing customer is disabled") 15 | ErrExistingAccountWithPendingDues = errors.New("existing account with pending dues found") 16 | ) 17 | -------------------------------------------------------------------------------- /billing/entitlement/error.go: -------------------------------------------------------------------------------- 1 | package entitlement 2 | 3 | import "fmt" 4 | 5 | var ( 6 | ErrPlanEntitlementFailed = fmt.Errorf("plan entitlement failed") 7 | ) 8 | -------------------------------------------------------------------------------- /billing/product/errors.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrProductNotFound = errors.New("product not found") 9 | ErrPriceNotFound = errors.New("price not found") 10 | ErrInvalidDetail = errors.New("invalid product detail") 11 | 12 | ErrPerSeatLimitReached = errors.New("per seat limit reached") 13 | 14 | ErrInvalidFeatureDetail = errors.New("invalid feature detail") 15 | ErrFeatureNotFound = errors.New("feature not found") 16 | ) 17 | -------------------------------------------------------------------------------- /billing/product/filter.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | type Filter struct { 4 | PlanID string 5 | ProductID string 6 | ProductIDs []string 7 | } 8 | -------------------------------------------------------------------------------- /billing/stripetest/backend.go: -------------------------------------------------------------------------------- 1 | package stripetest 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/stripe/stripe-go/v79" 7 | 8 | "github.com/stripe/stripe-go/v79/form" 9 | ) 10 | 11 | type Backend interface { 12 | Call(method, path, key string, params stripe.ParamsContainer, v stripe.LastResponseSetter) error 13 | CallStreaming(method, path, key string, params stripe.ParamsContainer, v stripe.StreamingLastResponseSetter) error 14 | CallRaw(method, path, key string, body *form.Values, params *stripe.Params, v stripe.LastResponseSetter) error 15 | CallMultipart(method, path, key, boundary string, body *bytes.Buffer, params *stripe.Params, v stripe.LastResponseSetter) error 16 | SetMaxNetworkRetries(maxNetworkRetries int64) 17 | } 18 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "v1" 3 | plugins: 4 | - plugin: "buf.build/protocolbuffers/go:v1.30.0" 5 | out: "proto" 6 | opt: "paths=source_relative" 7 | - plugin: "buf.build/grpc/go:v1.3.0" 8 | out: "proto" 9 | opt: "paths=source_relative" 10 | - plugin: "buf.build/bufbuild/validate-go:v1.0.0" 11 | out: "proto" 12 | opt: "paths=source_relative" 13 | - plugin: "buf.build/grpc-ecosystem/gateway:v2.15.2" 14 | out: "proto" 15 | opt: "paths=source_relative" 16 | - plugin: "buf.build/grpc-ecosystem/openapiv2:v2.16.0" 17 | out: "proto" 18 | opt: 19 | - allow_merge=true 20 | - output_format=yaml 21 | - json_names_for_fields=false 22 | -------------------------------------------------------------------------------- /cmd/context.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "google.golang.org/grpc/metadata" 8 | ) 9 | 10 | func setCtxHeader(ctx context.Context, header string) context.Context { 11 | s := strings.Split(header, ":") 12 | key := s[0] 13 | val := s[1] 14 | 15 | md := metadata.New(map[string]string{key: val}) 16 | ctx = metadata.NewOutgoingContext(ctx, md) 17 | 18 | return ctx 19 | } 20 | -------------------------------------------------------------------------------- /cmd/errors.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/MakeNowJust/heredoc" 7 | ) 8 | 9 | var ( 10 | ErrClientConfigNotFound = errors.New(heredoc.Doc(` 11 | Frontier client config not found. 12 | 13 | Run "frontier config init" to initialize a new client config or 14 | Run "frontier help environment" for more information. 15 | `)) 16 | ErrClientConfigHostNotFound = errors.New(heredoc.Doc(` 17 | Frontier client config "host" not found. 18 | 19 | Pass frontier server host with "--host" flag or 20 | set host in frontier config. 21 | 22 | Run "frontier config " or 23 | "frontier help environment" for more information. 24 | `)) 25 | ErrClientNotAuthorized = errors.New(heredoc.Doc(` 26 | Frontier auth error. Frontier requires an auth header. 27 | 28 | Run "frontier help auth" for more information. 29 | `)) 30 | ) 31 | -------------------------------------------------------------------------------- /cmd/help.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/MakeNowJust/heredoc" 4 | 5 | var envHelp = map[string]string{ 6 | "short": "List of supported environment variables", 7 | "long": heredoc.Doc(` 8 | RAYSTACK_CONFIG_DIR: the directory where frontier will store configuration files. Default: 9 | "$XDG_CONFIG_HOME/raystack" or "$HOME/.config/raystack". 10 | NO_COLOR: set to any value to avoid printing ANSI escape sequences for color output. 11 | CLICOLOR: set to "0" to disable printing ANSI colors in output. 12 | `), 13 | } 14 | 15 | var authHelp = map[string]string{ 16 | "short": "Auth configs that need to be used with frontier", 17 | "long": heredoc.Doc(` 18 | Send an additional flag header with "key:value" format. 19 | Example: 20 | frontier create user -f user.yaml -H X-Frontier-Email:user@raystack.org 21 | `), 22 | } 23 | -------------------------------------------------------------------------------- /cmd/seed/organizations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "sample-org1", 4 | "title": "Sample Org 1", 5 | "metadata": { 6 | "description": "Sample Frontier Organization" 7 | } 8 | }, 9 | { 10 | "name": "sample-org2", 11 | "title": "Sample Org 2", 12 | "metadata": {} 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /cmd/seed/projects.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "compute-guard-project", 4 | "title": "Compute Guard Project", 5 | "metadata": { 6 | "description": "Sample project for enforcing security policies on compute order service" 7 | }, 8 | "org_id": "" 9 | }, 10 | { 11 | "name": "order-management-project", 12 | "title": "Order Management service", 13 | "metadata": { 14 | "description": "Sample project for enforcing security policies on order management service" 15 | }, 16 | "org_id": "" 17 | } 18 | ] -------------------------------------------------------------------------------- /cmd/seed/resource.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "org1-resource-1", 4 | "namespace": "compute/order", 5 | "principal": "user-id", 6 | "metadata": { 7 | "description": "Sample resource for compute order" 8 | } 9 | }, 10 | { 11 | "name": "org2-resource-1", 12 | "namespace": "database/order", 13 | "principal": "user-id", 14 | "metadata": { 15 | "description": "Sample resource for compute order" 16 | } 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /cmd/seed/roles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "compute_order_viewer", 4 | "title": "Compute Order Viewer", 5 | "metadata": {}, 6 | "permissions": ["compute/order:get", "compute/order:list"] 7 | }, 8 | { 9 | "name": "compute_order_manager", 10 | "title": "Compute Order Manager", 11 | "metadata": {}, 12 | "permissions": ["compute/order:get", "compute/order:list", "compute/order:create", "compute/order:delete"] 13 | }, 14 | { 15 | "name": "database_order_viewer", 16 | "title": "Database Order Viewer", 17 | "metadata": {}, 18 | "permissions": ["database/order:get"] 19 | }, 20 | { 21 | "name": "database_order_manager", 22 | "title": "Database Order Manager", 23 | "metadata": {}, 24 | "permissions": ["database/order:get", "database/order:update", "database/order:delete"] 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /cmd/seed/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "johndoe", 4 | "email": "sample.user.1@raystack.org", 5 | "title": "John Doe", 6 | "metadata": { 7 | "description": "This is a sample human user" 8 | } 9 | }, 10 | { 11 | "name": "janedee", 12 | "email": "sample.user.2@raystack.org", 13 | "title": "Jane Dee", 14 | "metadata": { 15 | "description": "Another sample human user" 16 | } 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/raystack/frontier/config" 7 | 8 | "github.com/raystack/salt/version" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // VersionCmd prints the version of the binary 13 | func versionCommand() *cobra.Command { 14 | return &cobra.Command{ 15 | Use: "version", 16 | Aliases: []string{"v"}, 17 | Short: "Print version information", 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | if config.Version == "" { 20 | fmt.Println("Version information not available") 21 | return nil 22 | } 23 | fmt.Println("Frontier: A secure and easy-to-use Authentication & Authorization Server") 24 | fmt.Printf("Version: %s\nBuild date: %s\nCommit: %s", config.Version, config.BuildDate, config.BuildCommit) 25 | fmt.Println(version.UpdateNotice(config.Version, "raystack/frontier")) 26 | return nil 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /config/build.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var ( 4 | // overridden by the build system 5 | Version = "dev" 6 | BuildCommit = "" 7 | BuildDate = "" 8 | ) 9 | -------------------------------------------------------------------------------- /config/init.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | _ "embed" 5 | "errors" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/mcuadros/go-defaults" 11 | "github.com/raystack/frontier/pkg/file" 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | func Init(configFile string) error { 16 | if file.Exist(configFile) { 17 | return errors.New("config file already exists") 18 | } 19 | 20 | cfg := &Frontier{} 21 | defaults.SetDefaults(cfg) 22 | 23 | data, err := yaml.Marshal(cfg) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if _, err := os.Stat(configFile); os.IsNotExist(err) { 29 | if !file.DirExists(configFile) { 30 | _ = os.MkdirAll(filepath.Dir(configFile), 0755) 31 | } 32 | } 33 | 34 | if err := ioutil.WriteFile(configFile, data, 0655); err != nil { 35 | return err 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /config/testdata/use_duration.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | db: 4 | url: postgres://frontier:@localhost:5432/frontier?sslmode=disable 5 | max_query_timeout: 500ms 6 | conn_max_life_time: 10ms 7 | max_idle_conns: 10 8 | 9 | spicedb: 10 | host: spicedb.localhost 11 | pre_shared_key: randomkey 12 | port: 50051 13 | -------------------------------------------------------------------------------- /core/audit/filter.go: -------------------------------------------------------------------------------- 1 | package audit 2 | 3 | import "time" 4 | 5 | type Filter struct { 6 | OrgID string 7 | Source string 8 | Action string 9 | 10 | StartTime time.Time 11 | EndTime time.Time 12 | 13 | IgnoreSystem bool 14 | } 15 | -------------------------------------------------------------------------------- /core/audit/io_repository.go: -------------------------------------------------------------------------------- 1 | package audit 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | type WriteOnlyRepository struct { 11 | writer io.Writer 12 | } 13 | 14 | func NewWriteOnlyRepository(writer io.Writer) *WriteOnlyRepository { 15 | return &WriteOnlyRepository{writer: writer} 16 | } 17 | 18 | func (r WriteOnlyRepository) Create(ctx context.Context, log *Log) error { 19 | err := json.NewEncoder(r.writer).Encode(log) 20 | return err 21 | } 22 | 23 | func (r WriteOnlyRepository) List(ctx context.Context, filter Filter) ([]Log, error) { 24 | return nil, fmt.Errorf("unsupported") 25 | } 26 | 27 | func (r WriteOnlyRepository) GetByID(ctx context.Context, s string) (Log, error) { 28 | return Log{}, fmt.Errorf("unsupported") 29 | } 30 | -------------------------------------------------------------------------------- /core/audit/noop_repository.go: -------------------------------------------------------------------------------- 1 | package audit 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/raystack/frontier/core/webhook" 7 | ) 8 | 9 | type NoopRepository struct{} 10 | 11 | func NewNoopRepository() *NoopRepository { 12 | return &NoopRepository{} 13 | } 14 | 15 | func (r NoopRepository) Create(ctx context.Context, log *Log) error { 16 | return nil 17 | } 18 | 19 | func (r NoopRepository) List(ctx context.Context, filter Filter) ([]Log, error) { 20 | return []Log{}, nil 21 | } 22 | 23 | func (r NoopRepository) GetByID(ctx context.Context, s string) (Log, error) { 24 | return Log{}, nil 25 | } 26 | 27 | type NoopWebhookService struct{} 28 | 29 | func NewNoopWebhookService() *NoopWebhookService { 30 | return &NoopWebhookService{} 31 | } 32 | 33 | func (s NoopWebhookService) Publish(ctx context.Context, e webhook.Event) error { 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /core/authenticate/errors.go: -------------------------------------------------------------------------------- 1 | package authenticate 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvalidID = errors.New("user id is invalid") 7 | ) 8 | -------------------------------------------------------------------------------- /core/authenticate/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/raystack/frontier/pkg/metadata" 7 | 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // Session is created on successful authentication of users 12 | type Session struct { 13 | ID uuid.UUID 14 | 15 | // UserID is a unique identifier for logged in users 16 | UserID string 17 | 18 | // AuthenticatedAt is set when a user is successfully authn 19 | AuthenticatedAt time.Time 20 | 21 | // ExpiresAt is ideally now() + lifespan of session, e.g. 7 days 22 | ExpiresAt time.Time 23 | CreatedAt time.Time 24 | 25 | Metadata metadata.Metadata 26 | } 27 | 28 | func (s Session) IsValid(now time.Time) bool { 29 | if s.ExpiresAt.After(now) && !s.AuthenticatedAt.IsZero() { 30 | return true 31 | } 32 | return false 33 | } 34 | -------------------------------------------------------------------------------- /core/authenticate/test_users/config.go: -------------------------------------------------------------------------------- 1 | package testusers 2 | 3 | type Config struct { 4 | Enabled bool `yaml:"enabled" mapstructure:"enabled"` 5 | Domain string `yaml:"domain" mapstructure:"domain"` 6 | OTP string `yaml:"otp" mapstructure:"otp"` 7 | } 8 | -------------------------------------------------------------------------------- /core/deleter/deleter.go: -------------------------------------------------------------------------------- 1 | package deleter 2 | 3 | import "fmt" 4 | 5 | var ( 6 | ErrDeleteNotAllowed = fmt.Errorf("deletion not allowed for billed accounts") 7 | ) 8 | -------------------------------------------------------------------------------- /core/domain/domain.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type Repository interface { 9 | Create(ctx context.Context, domain Domain) (Domain, error) 10 | Get(ctx context.Context, id string) (Domain, error) 11 | Update(ctx context.Context, domain Domain) (Domain, error) 12 | List(ctx context.Context, flt Filter) ([]Domain, error) 13 | Delete(ctx context.Context, id string) error 14 | DeleteExpiredDomainRequests(ctx context.Context) error 15 | } 16 | 17 | type Status string 18 | 19 | func (s Status) String() string { 20 | return string(s) 21 | } 22 | 23 | const ( 24 | Pending Status = "pending" 25 | Verified Status = "verified" 26 | ) 27 | 28 | type Domain struct { 29 | ID string 30 | Name string 31 | OrgID string 32 | Token string 33 | State Status 34 | UpdatedAt time.Time 35 | CreatedAt time.Time 36 | } 37 | -------------------------------------------------------------------------------- /core/domain/errors.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotExist = errors.New("org domain request does not exist") 7 | ErrInvalidDomain = errors.New("invalid domain. No such host found") 8 | ErrTXTrecordNotFound = errors.New("required TXT record not found for domain verification") 9 | ErrDomainsMisMatch = errors.New("user domain does not match the organization domain") 10 | ErrInvalidId = errors.New("invalid domain id") 11 | ErrDuplicateKey = errors.New("domain name already exists for that organization") 12 | ) 13 | -------------------------------------------------------------------------------- /core/domain/filter.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Filter struct { 4 | OrgID string 5 | State Status 6 | Name string 7 | } 8 | -------------------------------------------------------------------------------- /core/event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | type ProviderWebhookEvent struct { 4 | Name string 5 | Body []byte 6 | } 7 | -------------------------------------------------------------------------------- /core/event/publisher.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/raystack/frontier/core/audit" 7 | ) 8 | 9 | // ChanPublisher is a blocking audit event publisher 10 | type ChanPublisher struct { 11 | target chan<- audit.Log 12 | } 13 | 14 | func NewChanPublisher(target chan<- audit.Log) *ChanPublisher { 15 | return &ChanPublisher{ 16 | target: target, 17 | } 18 | } 19 | 20 | func (p *ChanPublisher) Publish(ctx context.Context, log audit.Log) { 21 | p.target <- log 22 | } 23 | -------------------------------------------------------------------------------- /core/group/errors.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotExist = errors.New("group doesn't exist") 7 | ErrInvalidUUID = errors.New("invalid syntax of uuid") 8 | ErrInvalidID = errors.New("group id is invalid") 9 | ErrConflict = errors.New("group already exist") 10 | ErrInvalidDetail = errors.New("invalid group detail") 11 | ErrListingGroupRelations = errors.New("error while listing relations") 12 | ErrFetchingUsers = errors.New("error while fetching users") 13 | ErrFetchingGroups = errors.New("error while fetching groups") 14 | ) 15 | -------------------------------------------------------------------------------- /core/group/filter.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | type Filter struct { 4 | // only one filter gets applied at a time 5 | 6 | SU bool // super user 7 | 8 | OrganizationID string 9 | State State 10 | WithMemberCount bool 11 | 12 | GroupIDs []string 13 | } 14 | -------------------------------------------------------------------------------- /core/invitation/config.go: -------------------------------------------------------------------------------- 1 | package invitation 2 | 3 | type Config struct { 4 | // WithRoles if set to true will allow roles to be passed in invitation, when the user accepts the 5 | // invite, the role will be assigned to the user 6 | WithRoles bool `yaml:"with_roles" mapstructure:"with_roles" default:"false"` 7 | 8 | MailTemplate MailTemplateConfig `yaml:"mail_template" mapstructure:"mail_template"` 9 | } 10 | 11 | type MailTemplateConfig struct { 12 | Subject string `yaml:"subject" mapstructure:"subject" default:"You have been invited to join an organization"` 13 | Body string `yaml:"body" mapstructure:"body" default:"
Hi {{.UserID}},

You have been invited to join an organization: {{.Organization}}. Login to your account to accept the invitation.


Thanks,
Team Frontier
"` 14 | } 15 | -------------------------------------------------------------------------------- /core/invitation/filter.go: -------------------------------------------------------------------------------- 1 | package invitation 2 | 3 | type Filter struct { 4 | OrgID string 5 | UserID string 6 | } 7 | -------------------------------------------------------------------------------- /core/invitation/invite.go: -------------------------------------------------------------------------------- 1 | package invitation 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | "github.com/raystack/frontier/pkg/metadata" 9 | ) 10 | 11 | var ( 12 | ErrNotFound = errors.New("invitation not found") 13 | ErrInviteExpired = errors.New("invitation expired") 14 | ErrAlreadyMember = errors.New("user already exists in organization") 15 | ) 16 | 17 | const ( 18 | DefaultExpiryDuration = 24 * time.Hour * 7 19 | ) 20 | 21 | type Invitation struct { 22 | ID uuid.UUID 23 | UserEmailID string 24 | OrgID string 25 | GroupIDs []string 26 | RoleIDs []string 27 | Metadata metadata.Metadata 28 | CreatedAt time.Time 29 | ExpiresAt time.Time 30 | } 31 | -------------------------------------------------------------------------------- /core/kyc/errors.go: -------------------------------------------------------------------------------- 1 | package kyc 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotExist = errors.New("org kyc doesn't exist") 7 | ErrKycLinkNotSet = errors.New("link cannot be empty") 8 | ErrInvalidUUID = errors.New("invalid syntax of uuid") 9 | ErrOrgDoesntExist = errors.New("org doesn't exist") 10 | ) 11 | -------------------------------------------------------------------------------- /core/kyc/kyc.go: -------------------------------------------------------------------------------- 1 | package kyc 2 | 3 | import "time" 4 | 5 | type KYC struct { 6 | OrgID string 7 | Status bool 8 | Link string 9 | 10 | CreatedAt time.Time 11 | UpdatedAt time.Time 12 | } 13 | -------------------------------------------------------------------------------- /core/kyc/service.go: -------------------------------------------------------------------------------- 1 | package kyc 2 | 3 | import "context" 4 | 5 | type Repository interface { 6 | GetByOrgID(context.Context, string) (KYC, error) 7 | List(context.Context) ([]KYC, error) 8 | Upsert(context.Context, KYC) (KYC, error) 9 | } 10 | 11 | type Service struct { 12 | repository Repository 13 | } 14 | 15 | func NewService(repository Repository) *Service { 16 | return &Service{ 17 | repository: repository, 18 | } 19 | } 20 | 21 | func (s Service) GetKyc(ctx context.Context, orgID string) (KYC, error) { 22 | return s.repository.GetByOrgID(ctx, orgID) 23 | } 24 | 25 | func (s Service) SetKyc(ctx context.Context, kyc KYC) (KYC, error) { 26 | return s.repository.Upsert(ctx, kyc) 27 | } 28 | 29 | func (s Service) ListKycs(ctx context.Context) ([]KYC, error) { 30 | return s.repository.List(ctx) 31 | } 32 | -------------------------------------------------------------------------------- /core/metaschema/errors.go: -------------------------------------------------------------------------------- 1 | package metaschema 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvalidID = errors.New("metaschema id is invalid") 7 | ErrNotExist = errors.New("metaschema doesn't exist") 8 | ErrConflict = errors.New("metaschema already exist") 9 | ErrInvalidDetail = errors.New("invalid metadata detail") 10 | ErrInvalidMetaSchema = errors.New("metadata schema validation failed") 11 | ) 12 | -------------------------------------------------------------------------------- /core/metaschema/metaschema.go: -------------------------------------------------------------------------------- 1 | package metaschema 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type Repository interface { 9 | Create(ctx context.Context, metaschema MetaSchema) (MetaSchema, error) 10 | Get(ctx context.Context, id string) (MetaSchema, error) 11 | Update(ctx context.Context, id string, metaschema MetaSchema) (MetaSchema, error) 12 | List(ctx context.Context) ([]MetaSchema, error) 13 | Delete(ctx context.Context, id string) (string, error) 14 | MigrateDefaults(ctx context.Context) error 15 | } 16 | 17 | // MetaSchema represents metadata schema to be validated for users/ groups/ organisations / roles 18 | type MetaSchema struct { 19 | ID string 20 | Name string 21 | Schema string 22 | CreatedAt time.Time 23 | UpdatedAt time.Time 24 | } 25 | -------------------------------------------------------------------------------- /core/namespace/errors.go: -------------------------------------------------------------------------------- 1 | package namespace 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvalidID = errors.New("namespace id is invalid") 7 | ErrNotExist = errors.New("namespace doesn't exist") 8 | ErrConflict = errors.New("namespace name already exist") 9 | ErrInvalidDetail = errors.New("invalid namespace detail") 10 | ) 11 | -------------------------------------------------------------------------------- /core/namespace/namespace.go: -------------------------------------------------------------------------------- 1 | package namespace 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/raystack/frontier/pkg/metadata" 9 | ) 10 | 11 | type Repository interface { 12 | Get(ctx context.Context, id string) (Namespace, error) 13 | Upsert(ctx context.Context, ns Namespace) (Namespace, error) 14 | List(ctx context.Context) ([]Namespace, error) 15 | Update(ctx context.Context, ns Namespace) (Namespace, error) 16 | } 17 | 18 | type Namespace struct { 19 | ID string 20 | Name string 21 | Metadata metadata.Metadata 22 | CreatedAt time.Time 23 | UpdatedAt time.Time 24 | } 25 | 26 | func CreateID(backend, resourceType string) string { 27 | if resourceType == "" { 28 | return backend 29 | } 30 | 31 | return fmt.Sprintf("%s/%s", backend, resourceType) 32 | } 33 | -------------------------------------------------------------------------------- /core/namespace/service.go: -------------------------------------------------------------------------------- 1 | package namespace 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Service struct { 8 | repository Repository 9 | } 10 | 11 | func NewService(repository Repository) *Service { 12 | return &Service{ 13 | repository: repository, 14 | } 15 | } 16 | 17 | func (s Service) Get(ctx context.Context, id string) (Namespace, error) { 18 | return s.repository.Get(ctx, id) 19 | } 20 | 21 | func (s Service) Upsert(ctx context.Context, ns Namespace) (Namespace, error) { 22 | return s.repository.Upsert(ctx, ns) 23 | } 24 | 25 | func (s Service) List(ctx context.Context) ([]Namespace, error) { 26 | return s.repository.List(ctx) 27 | } 28 | 29 | func (s Service) Update(ctx context.Context, ns Namespace) (Namespace, error) { 30 | updatedNamespace, err := s.repository.Update(ctx, ns) 31 | if err != nil { 32 | return Namespace{}, err 33 | } 34 | return updatedNamespace, nil 35 | } 36 | -------------------------------------------------------------------------------- /core/organization/errors.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotExist = errors.New("org doesn't exist") 7 | ErrInvalidUUID = errors.New("invalid syntax of uuid") 8 | ErrInvalidID = errors.New("org id is invalid") 9 | ErrConflict = errors.New("org already exist") 10 | ErrInvalidDetail = errors.New("invalid org detail") 11 | ErrDisabled = errors.New("org is disabled") 12 | ) 13 | -------------------------------------------------------------------------------- /core/organization/filter.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import ( 4 | "github.com/raystack/frontier/pkg/pagination" 5 | ) 6 | 7 | type Filter struct { 8 | UserID string 9 | 10 | IDs []string 11 | State State 12 | 13 | Pagination *pagination.Pagination 14 | } 15 | -------------------------------------------------------------------------------- /core/organization/organization.go: -------------------------------------------------------------------------------- 1 | package organization 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/raystack/frontier/internal/bootstrap/schema" 7 | 8 | "github.com/raystack/frontier/pkg/metadata" 9 | ) 10 | 11 | type State string 12 | 13 | func (s State) String() string { 14 | return string(s) 15 | } 16 | 17 | const ( 18 | Enabled State = "enabled" 19 | Disabled State = "disabled" 20 | 21 | AdminPermission = schema.UpdatePermission 22 | AdminRelation = schema.OwnerRelationName 23 | AdminRole = schema.RoleOrganizationOwner 24 | MemberRole = schema.RoleOrganizationViewer 25 | ) 26 | 27 | type Organization struct { 28 | ID string 29 | Name string 30 | Title string 31 | Metadata metadata.Metadata 32 | State State 33 | Avatar string 34 | 35 | CreatedAt time.Time 36 | UpdatedAt time.Time 37 | 38 | // BillingID is the identifier of the organization in the billing engine 39 | BillingID string 40 | } 41 | -------------------------------------------------------------------------------- /core/permission/errors.go: -------------------------------------------------------------------------------- 1 | package permission 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvalidID = errors.New("permission id is invalid") 7 | ErrNotExist = errors.New("permission doesn't exist") 8 | ErrInvalidDetail = errors.New("invalid permission detail") 9 | ) 10 | -------------------------------------------------------------------------------- /core/permission/filter.go: -------------------------------------------------------------------------------- 1 | package permission 2 | 3 | type Filter struct { 4 | Namespace string 5 | Slugs []string 6 | } 7 | -------------------------------------------------------------------------------- /core/policy/errors.go: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotExist = errors.New("policies doesn't exist") 7 | ErrInvalidUUID = errors.New("invalid syntax of uuid") 8 | ErrInvalidID = errors.New("policy id is invalid") 9 | ErrConflict = errors.New("policy already exist") 10 | ErrInvalidDetail = errors.New("invalid policy detail") 11 | ) 12 | -------------------------------------------------------------------------------- /core/policy/filter.go: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | type Filter struct { 4 | OrgID string 5 | ProjectID string 6 | GroupID string 7 | RoleID string 8 | RoleIDs []string 9 | 10 | PrincipalType string 11 | PrincipalID string 12 | PrincipalIDs []string 13 | ResourceType string 14 | } 15 | -------------------------------------------------------------------------------- /core/preference/filter.go: -------------------------------------------------------------------------------- 1 | package preference 2 | 3 | type Filter struct { 4 | OrgID string `json:"org_id"` 5 | ProjectID string `json:"project_id"` 6 | UserID string `json:"user_id"` 7 | GroupID string `json:"group_id"` 8 | ResourceID string `json:"resource_id"` 9 | ResourceType string `json:"resource_type"` 10 | } 11 | -------------------------------------------------------------------------------- /core/preference/validator.go: -------------------------------------------------------------------------------- 1 | package preference 2 | 3 | import ( 4 | "strings" 5 | 6 | "golang.org/x/exp/slices" 7 | ) 8 | 9 | type PreferenceValidator interface { 10 | Validate(value string) bool 11 | } 12 | 13 | type BooleanValidator struct { 14 | allowedValues []string 15 | } 16 | 17 | func NewBooleanValidator() *BooleanValidator { 18 | return &BooleanValidator{ 19 | allowedValues: []string{"true", "false"}, 20 | } 21 | } 22 | 23 | func (v *BooleanValidator) Validate(value string) bool { 24 | return slices.Contains(v.allowedValues, value) 25 | } 26 | 27 | type TextValidator struct{} 28 | 29 | func NewTextValidator() *TextValidator { 30 | return &TextValidator{} 31 | } 32 | 33 | func (v *TextValidator) Validate(value string) bool { 34 | if strings.TrimSpace(value) != "" { 35 | return true // accepts any non-empty string 36 | } 37 | return false 38 | } 39 | -------------------------------------------------------------------------------- /core/project/errors.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotExist = errors.New("project or its relations doesn't exist") 7 | ErrInvalidUUID = errors.New("invalid syntax of uuid") 8 | ErrInvalidID = errors.New("project id is invalid") 9 | ErrConflict = errors.New("project already exist") 10 | ErrInvalidDetail = errors.New("invalid project detail") 11 | ) 12 | -------------------------------------------------------------------------------- /core/project/filter.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import "github.com/raystack/frontier/pkg/pagination" 4 | 5 | type Filter struct { 6 | OrgID string 7 | WithMemberCount bool 8 | ProjectIDs []string 9 | State State 10 | // NonInherited filters out projects that are inherited from access given through an organization 11 | NonInherited bool 12 | Pagination *pagination.Pagination 13 | } 14 | -------------------------------------------------------------------------------- /core/prospect/errors.go: -------------------------------------------------------------------------------- 1 | package prospect 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvalidEmail = errors.New("invalid email") 7 | ErrEmailActivityAlreadyExists = errors.New("email and activity combination already exists") 8 | ErrNotExist = errors.New("prospect does not exist") 9 | ErrInvalidUUID = errors.New("invalid syntax of uuid") 10 | ) 11 | -------------------------------------------------------------------------------- /core/relation/errors.go: -------------------------------------------------------------------------------- 1 | package relation 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotExist = errors.New("relation doesn't exist") 7 | ErrInvalidUUID = errors.New("invalid syntax of uuid") 8 | ErrInvalidID = errors.New("relation id is invalid") 9 | ErrConflict = errors.New("relation already exist") 10 | ErrInvalidDetail = errors.New("invalid relation detail") 11 | ErrCreatingRelationInStore = errors.New("error while creating relation") 12 | ErrCreatingRelationInAuthzEngine = errors.New("error while creating relation in authz engine") 13 | ErrFetchingUser = errors.New("error while fetching user") 14 | ) 15 | -------------------------------------------------------------------------------- /core/resource/errors.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotExist = errors.New("resource doesn't exist") 7 | ErrInvalidUUID = errors.New("invalid syntax of uuid") 8 | ErrInvalidID = errors.New("resource id is invalid") 9 | ErrInvalidURN = errors.New("resource urn is invalid") 10 | ErrConflict = errors.New("resource already exist") 11 | ErrInvalidDetail = errors.New("invalid resource detail") 12 | ) 13 | -------------------------------------------------------------------------------- /core/resource/filter.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | type Filter struct { 4 | ProjectID string 5 | UserID string 6 | ServiceUserID string 7 | NamespaceID string 8 | } 9 | -------------------------------------------------------------------------------- /core/role/errors.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotExist = errors.New("role doesn't exist") 7 | ErrInvalidID = errors.New("role id is invalid") 8 | ErrConflict = errors.New("role name already exist") 9 | ErrInvalidDetail = errors.New("invalid role detail") 10 | ) 11 | -------------------------------------------------------------------------------- /core/role/filter.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | type Filter struct { 4 | OrgID string 5 | Scopes []string 6 | IDs []string 7 | } 8 | -------------------------------------------------------------------------------- /core/role/role.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/raystack/frontier/pkg/metadata" 8 | ) 9 | 10 | type State string 11 | 12 | func (s State) String() string { 13 | return string(s) 14 | } 15 | 16 | const ( 17 | Enabled State = "enabled" 18 | Disabled State = "disabled" 19 | ) 20 | 21 | type Repository interface { 22 | Get(ctx context.Context, id string) (Role, error) 23 | GetByName(ctx context.Context, orgID, name string) (Role, error) 24 | List(ctx context.Context, f Filter) ([]Role, error) 25 | Upsert(ctx context.Context, role Role) (Role, error) 26 | Update(ctx context.Context, toUpdate Role) (Role, error) 27 | Delete(ctx context.Context, roleID string) error 28 | } 29 | 30 | type Role struct { 31 | ID string 32 | OrgID string 33 | Name string 34 | Title string 35 | Permissions []string 36 | State State 37 | Scopes []string // used for filtering 38 | Metadata metadata.Metadata 39 | CreatedAt time.Time 40 | UpdatedAt time.Time 41 | } 42 | -------------------------------------------------------------------------------- /core/serviceuser/errors.go: -------------------------------------------------------------------------------- 1 | package serviceuser 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotExist = errors.New("service user doesn't exist") 7 | ErrCredNotExist = errors.New("service user credential doesn't exist") 8 | ErrInvalidCred = errors.New("service user credential is invalid") 9 | ErrInvalidID = errors.New("service user id is invalid") 10 | ErrInvalidKeyID = errors.New("service user key is invalid") 11 | ErrConflict = errors.New("service user already exist") 12 | ErrEmptyKey = errors.New("empty key") 13 | ErrDisabled = errors.New("service user is disabled") 14 | ) 15 | -------------------------------------------------------------------------------- /core/serviceuser/filter.go: -------------------------------------------------------------------------------- 1 | package serviceuser 2 | 3 | type Filter struct { 4 | ServiceUserID string 5 | ServiceUserIDs []string 6 | 7 | OrgID string 8 | IsKey bool 9 | IsSecret bool 10 | State State 11 | } 12 | -------------------------------------------------------------------------------- /core/user/errors.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotExist = errors.New("user doesn't exist") 7 | ErrInvalidID = errors.New("user id is invalid") 8 | ErrInvalidEmail = errors.New("user email is invalid") 9 | ErrConflict = errors.New("user already exist") 10 | ErrInvalidDetails = errors.New("invalid user details (name|email)") 11 | ErrMissingName = errors.New("user name is missing") 12 | ErrEmptyKey = errors.New("empty key") 13 | ErrKeyAlreadyExists = errors.New("key already exist") 14 | ErrKeyDoesNotExists = errors.New("key does not exist") 15 | ErrMissingEmail = errors.New("user email is missing") 16 | ErrInvalidUUID = errors.New("invalid syntax of uuid") 17 | ErrDisabled = errors.New("user is disabled") 18 | ErrNoUsersFound = errors.New("no users found") 19 | ) 20 | -------------------------------------------------------------------------------- /core/user/filter.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | type Filter struct { 4 | // only one filter gets applied at a time 5 | 6 | Limit int32 7 | Page int32 8 | 9 | Keyword string 10 | OrgID string 11 | GroupID string 12 | State State 13 | } 14 | -------------------------------------------------------------------------------- /core/webhook/config.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | type Config struct { 4 | EncryptionKey string `yaml:"encryption_key" mapstructure:"encryption_key" default:"hash-secret-should-be-32-chars--"` 5 | } 6 | -------------------------------------------------------------------------------- /core/webhook/errors.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotFound = errors.New("webhook doesn't exist") 7 | ErrInvalidDetail = errors.New("invalid webhook details") 8 | ErrConflict = errors.New("webhook already exist") 9 | ErrInvalidUUID = errors.New("invalid syntax of uuid") 10 | ErrDisabled = errors.New("webhook is disabled") 11 | ) 12 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/Frontier_23June2024_HighLevel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/Frontier_23June2024_HighLevel.png -------------------------------------------------------------------------------- /docs/docs/authn/user_auth_google_oidc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/authn/user_auth_google_oidc.png -------------------------------------------------------------------------------- /docs/docs/authn/user_auth_session_accesstoken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/authn/user_auth_session_accesstoken.png -------------------------------------------------------------------------------- /docs/docs/authn/user_auth_session_proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/authn/user_auth_session_proxy.png -------------------------------------------------------------------------------- /docs/docs/authn/user_auth_supported_strategy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/authn/user_auth_supported_strategy.png -------------------------------------------------------------------------------- /docs/docs/concepts/frontier-authorization-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/concepts/frontier-authorization-architecture.png -------------------------------------------------------------------------------- /docs/docs/concepts/frontier-proxy-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/concepts/frontier-proxy-architecture.png -------------------------------------------------------------------------------- /docs/docs/concepts/overall-proxy-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/concepts/overall-proxy-architecture.png -------------------------------------------------------------------------------- /docs/docs/contribution/development.md: -------------------------------------------------------------------------------- 1 | ## Running locally 2 | 3 |
4 | Dependencies: 5 | 6 | - Git 7 | - Go 1.20 or above 8 | - PostgreSQL 13.2 or above 9 | 10 |
11 | 12 | Clone the repo 13 | 14 | ``` 15 | git clone git@github.com:raystack/frontier.git 16 | ``` 17 | 18 | Install all the golang dependencies 19 | 20 | ``` 21 | make install 22 | ``` 23 | 24 | Optional: build frontier admin ui 25 | 26 | ``` 27 | make ui 28 | ``` 29 | 30 | Build frontier binary file 31 | 32 | ``` 33 | make build 34 | ``` 35 | 36 | Init config 37 | 38 | ``` 39 | cp internal/server/config.yaml config.yaml 40 | ./frontier config init 41 | ``` 42 | 43 | Run database migrations 44 | 45 | ``` 46 | ./frontier server migrate -c config.yaml 47 | ``` 48 | 49 | Start frontier server 50 | 51 | ``` 52 | ./frontier server start -c config.yaml 53 | ``` 54 | 55 | ## Running tests 56 | 57 | ```sh 58 | # Running all unit tests 59 | $ make test 60 | 61 | # Print code coverage 62 | $ make coverage 63 | ``` 64 | -------------------------------------------------------------------------------- /docs/docs/frontier-flow-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/frontier-flow-diagram.png -------------------------------------------------------------------------------- /docs/docs/support.md: -------------------------------------------------------------------------------- 1 | # Support 2 | -------------------------------------------------------------------------------- /docs/docs/tour/authn-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/tour/authn-1.png -------------------------------------------------------------------------------- /docs/docs/tour/authn-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/tour/authn-2.png -------------------------------------------------------------------------------- /docs/docs/tour/authn-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/tour/authn-3.png -------------------------------------------------------------------------------- /docs/docs/tour/authn-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/tour/authn-4.png -------------------------------------------------------------------------------- /docs/docs/tour/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Welcome to this tour of Frontier. In this tour, we will take you through setting up the Frontier's server and configuring it to a backend service demonstrating Frontier as a proxy. 4 | 5 | We are going to cover this tour in the following steps, and recommend you to do the same. 6 | 7 | - Server Setup and configuring a backend service 8 | - Registering an organization in Frontier 9 | - Registering a project under that organization 10 | - Registering a group under this organization 11 | - Adding users to the group 12 | - Checking permissions using `Zed` tool 13 | - Making a call to the backend service via Frontier(proxy) 14 | -------------------------------------------------------------------------------- /docs/docs/tour/permission-schema-after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/tour/permission-schema-after.png -------------------------------------------------------------------------------- /docs/docs/tour/permission-schema-before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/tour/permission-schema-before.png -------------------------------------------------------------------------------- /docs/docs/tour/server-start-cmd-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/tour/server-start-cmd-output.png -------------------------------------------------------------------------------- /docs/docs/tour/shield-connection-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/tour/shield-connection-config.png -------------------------------------------------------------------------------- /docs/docs/tour/shield-tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/tour/shield-tables.png -------------------------------------------------------------------------------- /docs/docs/tour/spicedb-connection-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/tour/spicedb-connection-config.png -------------------------------------------------------------------------------- /docs/docs/tour/spicedb-tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/docs/tour/spicedb-tables.png -------------------------------------------------------------------------------- /docs/docs/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/src/theme/Mermaid.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import mermaid from "mermaid"; 3 | 4 | mermaid.initialize({ 5 | startOnLoad: true 6 | }); 7 | 8 | const Mermaid = ({ chart }) => { 9 | useEffect(() => { 10 | mermaid.contentLoaded(); 11 | }, []); 12 | return
{chart}
; 13 | }; 14 | 15 | export default Mermaid; -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/docs/static/.nojekyll -------------------------------------------------------------------------------- /internal/bootstrap/testdata/compute_service.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - name: delete 3 | namespace: compute/order 4 | - name: update 5 | namespace: compute/order 6 | - name: get 7 | namespace: compute/order 8 | - name: create 9 | namespace: compute/order 10 | - name: get 11 | namespace: compute/receipt 12 | - name: update 13 | namespace: compute/receipt 14 | -------------------------------------------------------------------------------- /internal/metrics/db.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var DatabaseQueryLatency HistogramFunc 9 | 10 | func initDB() { 11 | DatabaseQueryLatency = createMeasureTime(promauto.NewHistogramVec(prometheus.HistogramOpts{ 12 | Name: "db_query_latency", 13 | Help: "Time took to execute database query", 14 | Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 30, 60}, 15 | }, []string{"collection", "operation"})) 16 | } 17 | -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | func Init() { 10 | initStripe() 11 | initDB() 12 | initService() 13 | } 14 | 15 | type HistogramFunc func(labelValue ...string) func() 16 | 17 | func createMeasureTime(prometheusMetric *prometheus.HistogramVec) HistogramFunc { 18 | return func(labelValue ...string) func() { 19 | start := time.Now() 20 | return func() { 21 | prometheusMetric.WithLabelValues(labelValue...).Observe(time.Since(start).Seconds()) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/metrics/service.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ServiceOprLatency HistogramFunc 9 | 10 | func initService() { 11 | ServiceOprLatency = createMeasureTime(promauto.NewHistogramVec(prometheus.HistogramOpts{ 12 | Name: "service_operation_latency", 13 | Help: "Time taken for service operation to complete", 14 | Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 30, 60}, 15 | }, []string{"service", "operation"})) 16 | } 17 | -------------------------------------------------------------------------------- /internal/store/blob/testdata/compute.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - name: delete 3 | namespace: compute/order 4 | - name: update 5 | namespace: compute/order 6 | - name: get 7 | namespace: compute/order 8 | - name: list 9 | namespace: compute/order 10 | - name: create 11 | namespace: compute/order 12 | 13 | roles: 14 | - name: compute_order_manager 15 | permissions: 16 | - compute/order:delete 17 | - compute/order:update 18 | - compute/order:get 19 | - compute/order:list 20 | - compute/order:create 21 | - name: compute_order_viewer 22 | permissions: 23 | - compute/order:list 24 | - compute/order:get 25 | - name: compute_order_owner 26 | permissions: 27 | - compute/order:delete 28 | - compute/order:update 29 | - compute/order:get 30 | - compute/order:create 31 | 32 | -------------------------------------------------------------------------------- /internal/store/blob/testdata/database.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - name: delete 3 | namespace: database/order 4 | - name: update 5 | namespace: database/order 6 | - name: get 7 | namespace: database/order -------------------------------------------------------------------------------- /internal/store/postgres/domain.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/raystack/frontier/core/domain" 7 | ) 8 | 9 | type Domain struct { 10 | ID string `db:"id"` 11 | OrgID string `db:"org_id"` 12 | Name string `db:"name"` 13 | Token string `db:"token"` 14 | State string `db:"state"` 15 | UpdatedAt time.Time `db:"updated_at"` 16 | CreatedAt time.Time `db:"created_at"` 17 | } 18 | 19 | func (d Domain) transform() domain.Domain { 20 | return domain.Domain{ 21 | ID: d.ID, 22 | OrgID: d.OrgID, 23 | Name: d.Name, 24 | Token: d.Token, 25 | State: domain.Status(d.State), 26 | UpdatedAt: d.UpdatedAt, 27 | CreatedAt: d.CreatedAt, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/store/postgres/errors.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrDuplicateKey = errors.New("duplicate key") 7 | ErrCheckViolation = errors.New("check constraint violation") 8 | ErrForeignKeyViolation = errors.New("foreign key violation") 9 | ErrInvalidTextRepresentation = errors.New("invalid input syntax type") 10 | ErrInvalidID = errors.New("invalid id") 11 | ) 12 | -------------------------------------------------------------------------------- /internal/store/postgres/kyc.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/raystack/frontier/core/kyc" 7 | 8 | "database/sql" 9 | ) 10 | 11 | type KYC struct { 12 | OrgID string `db:"org_id"` 13 | Status bool `db:"status"` 14 | Link string `db:"link"` 15 | CreatedAt time.Time `db:"created_at"` 16 | UpdatedAt time.Time `db:"updated_at"` 17 | DeletedAt sql.NullTime `db:"deleted_at"` 18 | } 19 | 20 | func (from KYC) transformToKyc() (kyc.KYC, error) { 21 | return kyc.KYC{ 22 | OrgID: from.OrgID, 23 | Status: from.Status, 24 | Link: from.Link, 25 | CreatedAt: from.CreatedAt, 26 | UpdatedAt: from.UpdatedAt, 27 | }, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/store/postgres/metaschema.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/raystack/frontier/core/metaschema" 7 | ) 8 | 9 | type MetaSchema struct { 10 | ID string `db:"id"` 11 | Name string `db:"name"` 12 | Schema string `db:"schema"` 13 | CreatedAt time.Time `db:"created_at"` 14 | UpdatedAt time.Time `db:"updated_at"` 15 | } 16 | 17 | func (from MetaSchema) tranformtoMetadataSchema() metaschema.MetaSchema { 18 | return metaschema.MetaSchema{ 19 | ID: from.ID, 20 | Name: from.Name, 21 | Schema: from.Schema, 22 | CreatedAt: from.CreatedAt, 23 | UpdatedAt: from.UpdatedAt, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/store/postgres/metaschemas/group.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "group metadata", 3 | "description": "JSON-schema for validating group metadata", 4 | "type": "object", 5 | "properties": { 6 | "label": { 7 | "title": "Label", 8 | "description": "Additional context about the group", 9 | "type": "object" 10 | }, 11 | "description": { 12 | "title": "Description", 13 | "description": "Some additional information for the group", 14 | "type": "string" 15 | } 16 | }, 17 | "additionalProperties": true 18 | } -------------------------------------------------------------------------------- /internal/store/postgres/metaschemas/org.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "organization metadata", 3 | "description": "JSON-schema for validating organization metadata", 4 | "type": "object", 5 | "properties": { 6 | "label": { 7 | "title": "Label", 8 | "description": "Additional context about the organization", 9 | "type": "object" 10 | }, 11 | "description": { 12 | "title": "Description", 13 | "description": "Some additional information for the organization", 14 | "type": "string" 15 | } 16 | }, 17 | "additionalProperties": true 18 | } 19 | -------------------------------------------------------------------------------- /internal/store/postgres/metaschemas/prospect.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "prospect metadata", 3 | "description": "JSON-schema for validating prospect metadata", 4 | "type": "object", 5 | "properties": { 6 | "medium": { 7 | "title": "Medium", 8 | "description": "Additional medium data for the prospect", 9 | "type": "string" 10 | } 11 | }, 12 | "additionalProperties": false 13 | } -------------------------------------------------------------------------------- /internal/store/postgres/metaschemas/role.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "role metadata", 3 | "description": "JSON-schema for validating role metadata", 4 | "type": "object", 5 | "properties": { 6 | "label": { 7 | "title": "Label", 8 | "description": "Additional context about the user", 9 | "type": "object" 10 | }, 11 | "description": { 12 | "title": "Description", 13 | "description": "Some additional information for the role", 14 | "type": "string" 15 | } 16 | }, 17 | "additionalProperties": true 18 | } 19 | -------------------------------------------------------------------------------- /internal/store/postgres/metaschemas/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "user metadata", 3 | "description": "JSON-schema for validating user metadata", 4 | "type": "object", 5 | "properties": { 6 | "label": { 7 | "title": "Label", 8 | "description": "Additional context about the user", 9 | "type": "object" 10 | }, 11 | "description": { 12 | "title": "Description", 13 | "description": "Some additional information for the user", 14 | "type": "string" 15 | } 16 | }, 17 | "additionalProperties": true 18 | } 19 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20230507191406_create_base_tables.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS namespaces CASCADE; 2 | DROP TABLE IF EXISTS organizations CASCADE; 3 | DROP TABLE IF EXISTS projects CASCADE; 4 | DROP TABLE IF EXISTS groups CASCADE; 5 | DROP TABLE IF EXISTS users CASCADE; 6 | DROP TABLE IF EXISTS actions CASCADE; 7 | DROP TABLE IF EXISTS permissions CASCADE; 8 | DROP TABLE IF EXISTS roles CASCADE; 9 | DROP TABLE IF EXISTS relations CASCADE; 10 | DROP TABLE IF EXISTS resources CASCADE; 11 | DROP TABLE IF EXISTS policies CASCADE; 12 | DROP TABLE IF EXISTS flows CASCADE; 13 | DROP TABLE IF EXISTS sessions CASCADE; 14 | DROP TABLE IF EXISTS metadata CASCADE; 15 | DROP TABLE IF EXISTS metadata_keys CASCADE; 16 | DROP TABLE IF EXISTS metaschema CASCADE; 17 | DROP TABLE IF EXISTS invitations CASCADE; 18 | DROP TABLE IF EXISTS auditlogs CASCADE; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20230731211912_create_domain_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS domains; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20230731211912_create_domain_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS domains ( 2 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | name text NOT NULL, 4 | org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, 5 | token text NOT NULL, 6 | state text NOT NULL DEFAULT 'pending', 7 | updated_at timestamptz NOT NULL DEFAULT NOW(), 8 | created_at timestamptz NOT NULL DEFAULT NOW(), 9 | CONSTRAINT org_id_name_unique UNIQUE (org_id, name) 10 | ); 11 | CREATE INDEX IF NOT EXISTS domains_org_id_idx ON domains(org_id); -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20230814084723_add_role_title.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE roles DROP COLUMN IF EXISTS title; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20230814084723_add_role_title.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE roles ADD COLUMN title text; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20230819134146_create_preferences_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS preferences; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20230819134146_create_preferences_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS preferences ( 2 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | name text NOT NULL, 4 | value text, 5 | resource_type text REFERENCES namespaces (name) ON DELETE CASCADE, 6 | resource_id text NOT NULL, 7 | created_at timestamptz NOT NULL DEFAULT NOW(), 8 | updated_at timestamptz NOT NULL DEFAULT NOW(), 9 | CONSTRAINT resource_type_name_unique UNIQUE (resource_type, resource_id, name) 10 | ); 11 | CREATE INDEX IF NOT EXISTS preferences_resource_type_resource_id_idx ON preferences(resource_type, resource_id); -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20230907010521_add_users_orgs_avatar.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users DROP COLUMN IF EXISTS avatar; 2 | ALTER TABLE organizations DROP COLUMN IF EXISTS avatar; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20230907010521_add_users_orgs_avatar.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN avatar text; 2 | ALTER TABLE organizations ADD COLUMN avatar text; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20230923183419_add_scopes_in_roles.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE roles DROP COLUMN IF EXISTS scopes; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20230923183419_add_scopes_in_roles.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE roles ADD COLUMN scopes text[]; 2 | CREATE INDEX roles_scopes_idx ON roles USING gin(scopes); -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20231012175641_create_billing_tables.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS billing_prices CASCADE; 2 | DROP TABLE IF EXISTS billing_features CASCADE; 3 | DROP TABLE IF EXISTS billing_subscriptions CASCADE; 4 | DROP TABLE IF EXISTS billing_plans CASCADE; 5 | DROP TABLE IF EXISTS billing_checkouts CASCADE; 6 | DROP TABLE IF EXISTS billing_customers; 7 | DROP TABLE IF EXISTS billing_transactions; 8 | 9 | DROP INDEX IF EXISTS policies_resource_id_resource_type_idx; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20231231021142_add_feature_behavior.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_features 2 | DROP COLUMN IF EXISTS behavior; 3 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20231231021142_add_feature_behavior.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_features 2 | ADD COLUMN behavior text NOT NULL DEFAULT 'basic'; 3 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240106162115_refactor_features.down.sql: -------------------------------------------------------------------------------- 1 | -- billing_features 2 | DROP TABLE IF EXISTS billing_features; 3 | 4 | -- billing_products to billing_features 5 | ALTER TABLE billing_products 6 | RENAME TO billing_features; 7 | 8 | DROP INDEX IF EXISTS billing_products_plan_ids_idx; 9 | DROP INDEX IF EXISTS billing_products_provider_id_idx; 10 | CREATE INDEX IF NOT EXISTS billing_features_plan_ids_idx ON billing_features USING GIN(plan_ids); 11 | CREATE INDEX IF NOT EXISTS billing_features_provider_id_idx ON billing_features(provider_id); 12 | 13 | -- billing_prices 14 | DROP INDEX IF EXISTS billing_prices_product_id_idx; 15 | ALTER TABLE billing_prices 16 | RENAME COLUMN product_id TO feature_id; 17 | CREATE INDEX IF NOT EXISTS billing_prices_feature_id_idx ON billing_prices(feature_id); -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240118131733_product_config.down.sql: -------------------------------------------------------------------------------- 1 | -- products 2 | ALTER TABLE billing_products DROP COLUMN IF EXISTS config; 3 | ALTER TABLE billing_products ADD COLUMN IF NOT EXISTS credit_amount bigint; 4 | 5 | -- plans 6 | ALTER TABLE billing_plans DROP COLUMN IF EXISTS on_start_credits; 7 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240118131733_product_config.up.sql: -------------------------------------------------------------------------------- 1 | -- handle credits in plans 2 | ALTER TABLE billing_plans ADD COLUMN IF NOT EXISTS on_start_credits bigint NOT NULL DEFAULT 0; 3 | 4 | -- remove credit_amount column from billing_products table 5 | ALTER TABLE billing_products DROP COLUMN IF EXISTS credit_amount; 6 | 7 | -- add configuration column to billing_products table 8 | ALTER TABLE billing_products ADD COLUMN IF NOT EXISTS config jsonb NOT NULL DEFAULT '{}'::jsonb; 9 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240124202702_subscription_changes.down.sql: -------------------------------------------------------------------------------- 1 | -- subscription 2 | ALTER TABLE billing_subscriptions DROP COLUMN IF EXISTS changes; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240124202702_subscription_changes.up.sql: -------------------------------------------------------------------------------- 1 | -- add change in subscriptions table 2 | ALTER TABLE billing_subscriptions ADD COLUMN IF NOT EXISTS changes jsonb NOT NULL DEFAULT '{}'::jsonb; 3 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240206233616_plan_trial_days.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_plans DROP COLUMN IF EXISTS trial_days; 2 | 3 | ALTER TABLE billing_subscriptions DROP COLUMN IF EXISTS trial_ends_at; 4 | ALTER TABLE billing_subscriptions ADD COLUMN IF NOT EXISTS trial_days int8; 5 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240206233616_plan_trial_days.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_plans ADD COLUMN IF NOT EXISTS trial_days int8; 2 | 3 | ALTER TABLE billing_subscriptions ADD COLUMN IF NOT EXISTS trial_ends_at timestamptz; 4 | ALTER TABLE billing_subscriptions DROP COLUMN IF EXISTS trial_days; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240212105822_invoice_table.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_subscriptions DROP COLUMN IF EXISTS current_period_start_at; 2 | ALTER TABLE billing_subscriptions DROP COLUMN IF EXISTS current_period_end_at; 3 | ALTER TABLE billing_subscriptions DROP COLUMN IF EXISTS billing_cycle_anchor_at; 4 | 5 | DROP TABLE IF EXISTS billing_invoices; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240301162743_checkout_subscription_config.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_checkouts DROP COLUMN IF EXISTS subscription_config; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240301162743_checkout_subscription_config.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_checkouts ADD COLUMN IF NOT EXISTS subscription_config jsonb NOT NULL DEFAULT '{}'::jsonb; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240307201705_feature_title.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_features DROP COLUMN IF EXISTS title; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240307201705_feature_title.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_features ADD COLUMN IF NOT EXISTS title text; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240312181615_transactions_user_id.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_transactions DROP COLUMN IF EXISTS user_id; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240312181615_transactions_user_id.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_transactions ADD COLUMN IF NOT EXISTS user_id text; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240409114211_billing_customers_tax.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_customers DROP COLUMN IF EXISTS tax; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240409114211_billing_customers_tax.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_customers ADD COLUMN IF NOT EXISTS tax jsonb DEFAULT '{}'::jsonb; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240413083012_serviceuser_credential_type.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE serviceuser_credentials DROP COLUMN IF EXISTS type; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240413083012_serviceuser_credential_type.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE serviceuser_credentials ADD COLUMN IF NOT EXISTS type text; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240427034606_webhook_endpoint_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS webhook_endpoints; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240427034606_webhook_endpoint_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS webhook_endpoints ( 2 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | description text, 4 | subscribed_events text[], 5 | headers jsonb NOT NULL DEFAULT '{}'::jsonb, 6 | url text NOT NULL, 7 | secrets text, 8 | state text NOT NULL DEFAULT 'enabled', 9 | metadata jsonb NOT NULL DEFAULT '{}'::jsonb, 10 | created_at timestamptz NOT NULL DEFAULT NOW(), 11 | updated_at timestamptz NOT NULL DEFAULT NOW() 12 | ); 13 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240502075306_billing_customer_provider_notnull.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_customers ALTER COLUMN provider_id SET NOT NULL; 2 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240502075306_billing_customer_provider_notnull.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_customers ALTER COLUMN provider_id DROP NOT NULL; 2 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240917183506_billing_customer_credit_min.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_customers DROP COLUMN IF EXISTS credit_min; 2 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20240917183506_billing_customer_credit_min.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_customers ADD COLUMN IF NOT EXISTS credit_min int8 DEFAULT 0; 2 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20241015201506_billing_invoice_items.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_invoices DROP COLUMN IF EXISTS items; 2 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20241015201506_billing_invoice_items.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_invoices ADD COLUMN IF NOT EXISTS items jsonb DEFAULT '{}'; 2 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20250220054302_org_kyc.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS organizations_kyc; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20250303090030_create_prospect_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS prospects CASCADE; 2 | DROP TYPE IF EXISTS subscription_status; -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20250410211534_billing_customer_due_in_days.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_customers DROP COLUMN IF EXISTS due_in_days; 2 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20250410211534_billing_customer_due_in_days.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_customers ADD COLUMN IF NOT EXISTS due_in_days integer DEFAULT 0; 2 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20250528135454_billing_customer_due_in_days_default_30.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_customers ALTER COLUMN due_in_days SET DEFAULT 0; 2 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/20250528135454_billing_customer_due_in_days_default_30.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE billing_customers ALTER COLUMN due_in_days SET DEFAULT 30; 2 | UPDATE billing_customers SET due_in_days = 30; 3 | -------------------------------------------------------------------------------- /internal/store/postgres/migrations/migrations.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import "embed" 4 | 5 | //go:embed *.sql 6 | var MigrationFs embed.FS 7 | 8 | const ResourcePath = "." 9 | -------------------------------------------------------------------------------- /internal/store/postgres/namespace.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "database/sql" 8 | 9 | "github.com/raystack/frontier/core/namespace" 10 | ) 11 | 12 | type Namespace struct { 13 | ID string `db:"id"` 14 | Name string `db:"name"` 15 | Metadata []byte `db:"metadata"` 16 | CreatedAt time.Time `db:"created_at"` 17 | UpdatedAt time.Time `db:"updated_at"` 18 | DeletedAt sql.NullTime `db:"deleted_at"` 19 | } 20 | 21 | func (from Namespace) transformToNamespace() (namespace.Namespace, error) { 22 | var unmarshalledMetadata map[string]any 23 | if len(from.Metadata) > 0 { 24 | if err := json.Unmarshal(from.Metadata, &unmarshalledMetadata); err != nil { 25 | return namespace.Namespace{}, err 26 | } 27 | } 28 | 29 | return namespace.Namespace{ 30 | ID: from.ID, 31 | Name: from.Name, 32 | Metadata: unmarshalledMetadata, 33 | CreatedAt: from.CreatedAt, 34 | UpdatedAt: from.UpdatedAt, 35 | }, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/store/postgres/preference.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | "github.com/raystack/frontier/core/preference" 8 | ) 9 | 10 | type Preference struct { 11 | ID uuid.UUID `db:"id"` 12 | Name string `db:"name"` 13 | Value string `db:"value"` 14 | ResourceType string `db:"resource_type"` 15 | ResourceID string `db:"resource_id"` 16 | CreatedAt time.Time `db:"created_at"` 17 | UpdatedAt time.Time `db:"updated_at"` 18 | } 19 | 20 | func (from Preference) transformToPreference() preference.Preference { 21 | return preference.Preference{ 22 | ID: from.ID.String(), 23 | Name: from.Name, 24 | Value: from.Value, 25 | ResourceType: from.ResourceType, 26 | ResourceID: from.ResourceID, 27 | CreatedAt: from.CreatedAt, 28 | UpdatedAt: from.UpdatedAt, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/store/postgres/testdata/mock-group.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "group-1", 4 | "metadata":{} 5 | }, 6 | { 7 | "name": "group-2", 8 | "metadata":{} 9 | }, 10 | { 11 | "name": "group-3", 12 | "metadata": { 13 | "label": "label", 14 | "description": "description" 15 | } 16 | }, 17 | { 18 | "name": "group-4", 19 | "metadata": {}, 20 | "state": "disabled" 21 | } 22 | ] -------------------------------------------------------------------------------- /internal/store/postgres/testdata/mock-invitation.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "nx", 4 | "id": "00000000-0000-0000-0000-000000000001" 5 | }, 6 | { 7 | "name": "ny", 8 | "id": "00000000-0000-0000-0000-000000000002" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /internal/store/postgres/testdata/mock-namespace.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "ns1" 4 | }, 5 | { 6 | "name": "ns2" 7 | }, 8 | { 9 | "name": "Back1 R1", 10 | "backend": "back1", 11 | "resourceType": "r1" 12 | }, 13 | { 14 | "name": "Back1 R2", 15 | "backend": "back1", 16 | "resourceType": "r2" 17 | }, 18 | { 19 | "name": "Back2 R1", 20 | "backend": "back2", 21 | "resourceType": "r1" 22 | }, 23 | { 24 | "name": "app/group" 25 | }, 26 | { 27 | "name": "app/project" 28 | }, 29 | { 30 | "name": "app/organization" 31 | }, 32 | { 33 | "name": "app/user" 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /internal/store/postgres/testdata/mock-organization-kyc.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "status": true, 4 | "link": "abcd" 5 | } 6 | ] -------------------------------------------------------------------------------- /internal/store/postgres/testdata/mock-organization.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "org-1", 4 | "metadata":{} 5 | }, 6 | { 7 | "name": "org-2", 8 | "metadata": { 9 | "label": {}, 10 | "description": "description" 11 | } 12 | } 13 | ] -------------------------------------------------------------------------------- /internal/store/postgres/testdata/mock-permission.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "permission-post", 4 | "namespaceId": "ns1", 5 | "slug": "ns1_resource_post" 6 | }, 7 | { 8 | "name": "permission-get", 9 | "namespaceId": "ns1", 10 | "slug": "ns1_resource_get" 11 | }, 12 | { 13 | "name": "permission-put", 14 | "namespaceId": "ns2", 15 | "slug": "ns2_resource_put" 16 | }, 17 | { 18 | "name": "permission-delete", 19 | "namespaceId": "ns2", 20 | "slug": "ns2_resource_delete" 21 | } 22 | ] -------------------------------------------------------------------------------- /internal/store/postgres/testdata/mock-policy.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "resource_type": "ns1", 4 | "principal_type": "app/user" 5 | }, 6 | { 7 | "resource_type": "ns2", 8 | "principal_type": "app/user" 9 | }, 10 | { 11 | "resource_type": "ns1", 12 | "principal_type": "app/user" 13 | } 14 | ] -------------------------------------------------------------------------------- /internal/store/postgres/testdata/mock-project.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "project-1", 4 | "metadata":{} 5 | }, 6 | { 7 | "name": "project-2", 8 | "metadata": { 9 | "label": {}, 10 | "description": "description" 11 | } 12 | }, 13 | { 14 | "name": "project-3", 15 | "metadata":{} 16 | } 17 | ] -------------------------------------------------------------------------------- /internal/store/postgres/testdata/mock-prospect.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "00000000-0000-0000-0000-000000000001", 4 | "name": "test-1", 5 | "email": "test1@example.com", 6 | "phone": "1234567890", 7 | "activity": "activity-1", 8 | "status": "subscribed", 9 | "changed_at": "2025-01-02T00:00:00Z", 10 | "source": "source-1", 11 | "verified": true, 12 | "created_at": "2025-01-01T00:00", 13 | "updated_at": "2025-01-05T00:00", 14 | "metadata": { 15 | "medium": "test" 16 | } 17 | }, 18 | { 19 | "id": "00000000-0000-0000-0000-000000000002", 20 | "name": "test-2", 21 | "email": "test2@example.com", 22 | "phone": "", 23 | "activity": "activity-2", 24 | "status": "unsubscribed", 25 | "changed_at": "2025-03-02T00:00:00Z", 26 | "source": "source-1", 27 | "verified": false, 28 | "created_at": "2025-03-01T00:00", 29 | "updated_at": "2025-03-05T00:00", 30 | "metadata": {} 31 | } 32 | ] -------------------------------------------------------------------------------- /internal/store/postgres/testdata/mock-relation.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "subject": { 4 | "id": "uuid1", 5 | "namespace": "ns1" 6 | }, 7 | "object": { 8 | "id": "uuid2", 9 | "namespace": "ns1" 10 | }, 11 | "relation_name": "relation1" 12 | }, 13 | { 14 | "subject": { 15 | "id": "uuid3", 16 | "namespace": "ns2" 17 | }, 18 | "object": { 19 | "id": "uuid4", 20 | "namespace": "ns2" 21 | }, 22 | "relation_name": "relation2" 23 | } 24 | ] -------------------------------------------------------------------------------- /internal/store/postgres/testdata/mock-resource.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "resource-1", 4 | "urn": "resource-1-urn" 5 | }, 6 | { 7 | "name": "resource-2", 8 | "urn": "resource-2-urn" 9 | }, 10 | { 11 | "name": "resource-3", 12 | "urn": "resource-3-urn" 13 | } 14 | ] -------------------------------------------------------------------------------- /internal/store/postgres/testdata/mock-user.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "John Doe", 4 | "email": "john.doe@raystack.org", 5 | "name": "john_doe_slug", 6 | "metadata": {} 7 | }, 8 | { 9 | "title": "Jane Dee", 10 | "email": "jane.dee@raystack.org", 11 | "name": "john_dee_slug", 12 | "metadata": { 13 | "label": { 14 | "key": "value" 15 | }, 16 | "description": "description" 17 | } 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /internal/store/spicedb/schema_repository.go: -------------------------------------------------------------------------------- 1 | package spicedb 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/raystack/salt/log" 9 | 10 | authzedpb "github.com/authzed/authzed-go/proto/authzed/api/v1" 11 | ) 12 | 13 | type SchemaRepository struct { 14 | spiceDB *SpiceDB 15 | logger log.Logger 16 | } 17 | 18 | var ( 19 | ErrWritingSchema = errors.New("error in writing schema to spicedb") 20 | ) 21 | 22 | func NewSchemaRepository(logger log.Logger, spiceDB *SpiceDB) *SchemaRepository { 23 | return &SchemaRepository{ 24 | spiceDB: spiceDB, 25 | logger: logger, 26 | } 27 | } 28 | 29 | func (r SchemaRepository) WriteSchema(ctx context.Context, schema string) error { 30 | if r.logger.Level() == "debug" { 31 | fmt.Println(schema) 32 | } 33 | if _, err := r.spiceDB.client.WriteSchema(ctx, &authzedpb.WriteSchemaRequest{Schema: schema}); err != nil { 34 | return fmt.Errorf("%w: %s", ErrWritingSchema, err.Error()) 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/raystack/frontier/cmd" 8 | 9 | _ "github.com/authzed/authzed-go/proto/authzed/api/v0" 10 | ) 11 | 12 | func main() { 13 | cliConfig, err := cmd.LoadConfig() 14 | if err != nil { 15 | cliConfig = &cmd.Config{} 16 | } 17 | 18 | if err := cmd.New(cliConfig).Execute(); err != nil { 19 | fmt.Printf("%+v\n", err) 20 | os.Exit(1) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/crypt/key.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "io" 7 | ) 8 | 9 | // NewEncryptionKey generates a random 256-bit key 10 | func NewEncryptionKey() (*[32]byte, error) { 11 | key := [32]byte{} 12 | _, err := io.ReadFull(rand.Reader, key[:]) 13 | if err != nil { 14 | return nil, err 15 | } 16 | return &key, nil 17 | } 18 | 19 | func NewEncryptionKeyInHex() (string, error) { 20 | key, err := NewEncryptionKey() 21 | if err != nil { 22 | return "", err 23 | } 24 | return hex.EncodeToString(key[:]), nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/crypt/random.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | ) 7 | 8 | // GenerateRandomStringFromLetters generates a random string of the given length using the provided runes 9 | // this function panics if 10 | // - the provided length is less than 1 11 | // - if the provided runes are empty 12 | // - if os fails to read random bytes 13 | func GenerateRandomStringFromLetters(length int, letterRunes []rune) string { 14 | b := make([]rune, length) 15 | for i := range b { 16 | num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letterRunes)))) 17 | if err != nil { 18 | panic(err) 19 | } 20 | b[i] = letterRunes[num.Int64()] 21 | } 22 | return string(b) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/db/config.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "time" 4 | 5 | type Config struct { 6 | Driver string `yaml:"driver" mapstructure:"driver" default:"postgres"` 7 | URL string `yaml:"url" mapstructure:"url"` 8 | MaxIdleConns int `yaml:"max_idle_conns" mapstructure:"max_idle_conns" default:"10"` 9 | MaxOpenConns int `yaml:"max_open_conns" mapstructure:"max_open_conns" default:"10"` 10 | ConnMaxLifeTime time.Duration `yaml:"conn_max_life_time" mapstructure:"conn_max_life_time" default:"15m"` 11 | MaxQueryTimeout time.Duration `yaml:"max_query_timeout" mapstructure:"max_query_timeout" default:"5s"` 12 | } 13 | -------------------------------------------------------------------------------- /pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "errors" 4 | 5 | // These aliased values are added to avoid conflicting imports of standard `errors` 6 | // package and this `errors` package where these functions are needed. 7 | var ( 8 | Is = errors.Is 9 | As = errors.As 10 | New = errors.New 11 | ) 12 | 13 | var ( 14 | ErrUnauthenticated = errors.New("not authenticated") 15 | ErrForbidden = errors.New("not authorized") 16 | ErrInvalidUUID = errors.New("invalid syntax of uuid") 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/httputil/context.go: -------------------------------------------------------------------------------- 1 | package httputil 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type ( 8 | contextRequestBodyKey struct{} 9 | contextPathParamsKey struct{} 10 | ) 11 | 12 | func SetContextWithRequestBody(ctx context.Context, body []byte) context.Context { 13 | return context.WithValue(ctx, contextRequestBodyKey{}, body) 14 | } 15 | 16 | func GetRequestBodyFromContext(ctx context.Context) ([]byte, bool) { 17 | body, ok := ctx.Value(contextRequestBodyKey{}).([]byte) 18 | return body, ok 19 | } 20 | 21 | func SetContextWithPathParams(ctx context.Context, params map[string]string) context.Context { 22 | return context.WithValue(ctx, contextPathParamsKey{}, params) 23 | } 24 | 25 | func GetPathParamsFromContext(ctx context.Context) (map[string]string, bool) { 26 | params, ok := ctx.Value(contextPathParamsKey{}).(map[string]string) 27 | return params, ok 28 | } 29 | -------------------------------------------------------------------------------- /pkg/httputil/header.go: -------------------------------------------------------------------------------- 1 | package httputil 2 | 3 | const ( 4 | HeaderUserAgent = "User-Agent" 5 | HeaderXUser = "X-User" 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/logger/config.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | type Config struct { 4 | // log level - debug, info, warning, error, fatal 5 | Level string `yaml:"level" mapstructure:"level" default:"info" json:"level,omitempty"` 6 | 7 | // format strategy - plain, json 8 | Format string `yaml:"format" mapstructure:"format" default:"json" json:"format,omitempty"` 9 | 10 | // audit system events - none(default), stdout, db 11 | AuditEvents string `yaml:"audit_events" mapstructure:"audit_events" default:"none" json:"audit_events,omitempty"` 12 | 13 | // IgnoredAuditEvents contains list of events which should be ignored in audit logs 14 | IgnoredAuditEvents []string `yaml:"ignored_audit_events" mapstructure:"ignored_audit_events" json:"ignored_audit_events,omitempty"` 15 | } 16 | -------------------------------------------------------------------------------- /pkg/metadata/metadata.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "google.golang.org/protobuf/types/known/structpb" 5 | ) 6 | 7 | // Metadata is a structure to store dynamic values in 8 | // frontier. it could be use as an additional information 9 | // of a specific entity 10 | type Metadata map[string]any 11 | 12 | // ToStructPB transforms Metadata to *structpb.Struct 13 | func (m Metadata) ToStructPB() (*structpb.Struct, error) { 14 | newMap := make(map[string]any) 15 | 16 | for key, value := range m { 17 | newMap[key] = value 18 | } 19 | 20 | return structpb.NewStruct(newMap) 21 | } 22 | 23 | // Build transforms a Metadata from map[string]any 24 | func Build(m map[string]any) Metadata { 25 | newMap := make(Metadata) 26 | for key, value := range m { 27 | newMap[key] = value 28 | } 29 | return newMap 30 | } 31 | 32 | // FromString transforms a Metadata from map[string]string 33 | func FromString(m map[string]string) Metadata { 34 | newMap := make(Metadata) 35 | for key, value := range m { 36 | newMap[key] = value 37 | } 38 | return newMap 39 | } 40 | -------------------------------------------------------------------------------- /pkg/pagination/pagination.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | const ( 4 | DefaultPageSize = 1000 5 | DefaultPageNum = 1 6 | ) 7 | 8 | type Pagination struct { 9 | PageNum int32 10 | PageSize int32 11 | Count int32 // total number of records in DB 12 | } 13 | 14 | func NewPagination(pageNum, pageSize int32) *Pagination { 15 | if pageNum == 0 { 16 | pageNum = DefaultPageNum 17 | } 18 | if pageSize == 0 { 19 | pageSize = DefaultPageSize 20 | } 21 | 22 | return &Pagination{ 23 | PageNum: pageNum, 24 | PageSize: pageSize, 25 | } 26 | } 27 | 28 | func (p *Pagination) Offset() int32 { 29 | return p.PageSize * (p.PageNum - 1) 30 | } 31 | 32 | func (p *Pagination) SetCount(count int32) { 33 | p.Count = count 34 | } 35 | -------------------------------------------------------------------------------- /pkg/server/health/health.go: -------------------------------------------------------------------------------- 1 | package health 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc/codes" 7 | "google.golang.org/grpc/health/grpc_health_v1" 8 | "google.golang.org/grpc/status" 9 | ) 10 | 11 | type Handler struct{} 12 | 13 | func (h *Handler) Check(context.Context, *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) { 14 | return &grpc_health_v1.HealthCheckResponse{Status: grpc_health_v1.HealthCheckResponse_SERVING}, nil 15 | } 16 | 17 | func (h *Handler) Watch(*grpc_health_v1.HealthCheckRequest, grpc_health_v1.Health_WatchServer) error { 18 | // Example of how to register both methods but only implement the Check method. 19 | return status.Error(codes.Unimplemented, "unimplemented") 20 | } 21 | 22 | func NewHandler() *Handler { 23 | return &Handler{} 24 | } 25 | -------------------------------------------------------------------------------- /pkg/server/interceptors/audit.go: -------------------------------------------------------------------------------- 1 | package interceptors 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/raystack/frontier/core/audit" 7 | "google.golang.org/grpc" 8 | ) 9 | 10 | func UnaryCtxWithAudit(service *audit.Service) grpc.UnaryServerInterceptor { 11 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { 12 | ctx = audit.SetContextWithService(ctx, service) 13 | return handler(ctx, req) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pkg/server/interceptors/cors.go: -------------------------------------------------------------------------------- 1 | package interceptors 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/handlers" 7 | ) 8 | 9 | type CorsConfig struct { 10 | AllowedOrigins []string `yaml:"allowed_origins" mapstructure:"allowed_origins" default:""` 11 | AllowedMethods []string `yaml:"allowed_methods" mapstructure:"allowed_methods" default:"GET POST PUT DELETE PATCH"` 12 | AllowedHeaders []string `yaml:"allowed_headers" mapstructure:"allowed_headers" default:"Authorization"` 13 | ExposedHeaders []string `yaml:"exposed_headers" mapstructure:"exposed_headers" default:"Content-Type"` 14 | } 15 | 16 | func WithCors(h http.Handler, conf CorsConfig) http.Handler { 17 | return handlers.CORS( 18 | handlers.AllowedOrigins(conf.AllowedOrigins), 19 | handlers.AllowedMethods(conf.AllowedMethods), 20 | handlers.AllowedHeaders(conf.AllowedHeaders), 21 | handlers.ExposedHeaders(conf.ExposedHeaders), 22 | handlers.AllowCredentials(), 23 | )(h) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/server/interceptors/headers.go: -------------------------------------------------------------------------------- 1 | package interceptors 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" 7 | ) 8 | 9 | // GatewayHeaderMatcherFunc allows bypassing default runtime behaviour of prefixing headers with `grpc-gateway` 10 | func GatewayHeaderMatcherFunc(headerKeys map[string]bool) func(key string) (string, bool) { 11 | return func(key string) (string, bool) { 12 | if _, ok := headerKeys[strings.ToLower(key)]; ok { 13 | return key, true 14 | } 15 | return runtime.DefaultHeaderMatcher(key) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pkg/server/interceptors/passthrough_header.go: -------------------------------------------------------------------------------- 1 | package interceptors 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/raystack/frontier/core/authenticate" 8 | 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/metadata" 11 | ) 12 | 13 | func EnrichCtxWithPassthroughEmail(identityHeader string) grpc.UnaryServerInterceptor { 14 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { 15 | if len(identityHeader) == 0 { 16 | // if not configured, skip 17 | return handler(ctx, req) 18 | } 19 | 20 | md, ok := metadata.FromIncomingContext(ctx) 21 | if !ok { 22 | return "", fmt.Errorf("metadata for identity doesn't exist") 23 | } 24 | 25 | var email string 26 | if metadataValues := md.Get(identityHeader); len(metadataValues) > 0 { 27 | email = metadataValues[0] 28 | } 29 | 30 | ctx = authenticate.SetContextWithEmail(ctx, email) 31 | return handler(ctx, req) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/str/utils.go: -------------------------------------------------------------------------------- 1 | package str 2 | 3 | func DefaultStringIfEmpty(str string, defaultString string) string { 4 | if str != "" { 5 | return str 6 | } 7 | return defaultString 8 | } 9 | -------------------------------------------------------------------------------- /pkg/utils/domain.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "strings" 4 | 5 | func ExtractDomainFromEmail(email string) string { 6 | parts := strings.Split(email, "@") 7 | if len(parts) == 2 { 8 | return parts[1] 9 | } 10 | return "" 11 | } 12 | -------------------------------------------------------------------------------- /pkg/utils/jwk_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | "github.com/lestrrat-go/jwx/v2/jwk" 9 | "github.com/lestrrat-go/jwx/v2/jwt" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestBuildToken(t *testing.T) { 14 | issuer := "test" 15 | sub := uuid.New().String() 16 | validity := time.Minute * 10 17 | kid := uuid.New().String() 18 | newKey, err := CreateJWKWithKID(kid) 19 | assert.NoError(t, err) 20 | t.Run("create a valid token", func(t *testing.T) { 21 | got, err := BuildToken(newKey, issuer, sub, validity, nil) 22 | assert.NoError(t, err) 23 | parsedToken, err := jwt.ParseInsecure(got) 24 | assert.NoError(t, err) 25 | assert.Equal(t, issuer, parsedToken.Issuer()) 26 | assert.Equal(t, sub, parsedToken.Subject()) 27 | gotKid, _ := parsedToken.Get(jwk.KeyIDKey) 28 | assert.Equal(t, kid, gotKid) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/utils/pointers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // Bool returns a pointer to the bool value passed in. 4 | func Bool(v bool) *bool { 5 | return &v 6 | } 7 | 8 | // BoolValue returns the value of the bool pointer passed in or 9 | // false if the pointer is nil. 10 | func BoolValue(v *bool) bool { 11 | if v != nil { 12 | return *v 13 | } 14 | return false 15 | } 16 | -------------------------------------------------------------------------------- /pkg/utils/slice_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | _ "embed" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestAppendIfUnique(t *testing.T) { 13 | fmt.Println(AppendIfUnique([]string{"1", "2", "3"}, []string{"3", "4"})) 14 | assert.ElementsMatch(t, AppendIfUnique([]string{"1", "2", "3"}, []string{"3", "4"}), []string{"1", "2", "3", "4"}) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/utils/time.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "time" 4 | 5 | func AsTimeFromEpoch(unixEpoch int64) time.Time { 6 | if unixEpoch == 0 { 7 | return time.Time{} 8 | } 9 | return time.Unix(unixEpoch, 0) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/utils/uuid.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/mail" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | // NewString is type alias to `github.com/google/uuid`.NewString 10 | var NewString = uuid.NewString 11 | 12 | // IsValidUUID returns true if passed string in uuid format 13 | // defined by `github.com/google/uuid`.Parse 14 | // else return false 15 | func IsValidUUID(key string) bool { 16 | _, err := uuid.Parse(key) 17 | return err == nil 18 | } 19 | 20 | // IsNullUUID returns true if passed string is a null uuid or is not a valid uuid 21 | // defined by `github.com/google/uuid`.Parse and `github.com/google/uuid`.Nil respectively 22 | // else return false 23 | func IsNullUUID(key string) bool { 24 | k, err := uuid.Parse(key) 25 | return err != nil || k == uuid.Nil 26 | } 27 | 28 | func IsValidEmail(str string) bool { 29 | _, err := mail.ParseAddress(str) 30 | return err == nil 31 | } 32 | -------------------------------------------------------------------------------- /sdks/js/.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /sdks/js/.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "privatePackages": { "version": false, "tag": false }, 11 | "ignore": [] 12 | } 13 | -------------------------------------------------------------------------------- /sdks/js/.changeset/cool-terms-smell.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@raystack/frontier': patch 3 | --- 4 | 5 | Don't use private package for changelog 6 | -------------------------------------------------------------------------------- /sdks/js/.eslintignore: -------------------------------------------------------------------------------- 1 | .next 2 | .nuxt 3 | node_modules 4 | dist -------------------------------------------------------------------------------- /sdks/js/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@raystack/eslint-config'], 4 | settings: { 5 | next: { 6 | rootDir: ['apps/*/'] 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /sdks/js/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | dist 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # turbo 34 | .turbo 35 | 36 | # vercel 37 | .vercel -------------------------------------------------------------------------------- /sdks/js/.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | strict-peer-dependencies = false 3 | link-workspace-packages=true 4 | -------------------------------------------------------------------------------- /sdks/js/.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | .nuxt 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /sdks/js/packages/core/.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "requireBranch": false, 4 | "getLatestTagFromAllRefs": true, 5 | "push": false, 6 | "commit": false, 7 | "tag": false, 8 | "requireCleanWorkingDir": false 9 | }, 10 | "github": { 11 | "tag": false, 12 | "release": false 13 | }, 14 | "npm": { 15 | "ignoreVersion": true 16 | } 17 | } -------------------------------------------------------------------------------- /sdks/js/packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @raystack/frontier 2 | 3 | ## 0.0.12 4 | 5 | ### Patch Changes 6 | 7 | - c655b86: Fix table title column 8 | 9 | ## 0.0.11 10 | 11 | ### Patch Changes 12 | 13 | - 09371be0: feat: add modal to give project access to a team 14 | 15 | ## 0.0.10 16 | 17 | ### Patch Changes 18 | 19 | - 533747df: minor UI fixes 20 | 21 | ## 0.0.9 22 | 23 | ### Patch Changes 24 | 25 | - ebec10df: remove admin apis 26 | 27 | ## 0.0.8 28 | 29 | ### Patch Changes 30 | 31 | - 6838d9d9: use theme variables in loader colors 32 | 33 | ## 0.0.7 34 | 35 | ### Patch Changes 36 | 37 | - 62e13847: minor ui fixes 38 | - f696b18f: Add table loader 39 | 40 | ## 0.0.5 41 | 42 | ### Patch Changes 43 | 44 | - d0077d5f: updated router flow 45 | -------------------------------------------------------------------------------- /sdks/js/packages/core/api-client/index.ts: -------------------------------------------------------------------------------- 1 | import { V1Beta1 } from './V1Beta1'; 2 | 3 | export * from './data-contracts'; 4 | export const ApiClient = V1Beta1; -------------------------------------------------------------------------------- /sdks/js/packages/core/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: React.FunctionComponent>; 3 | export default content; 4 | } 5 | 6 | declare module '*.jpg' { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module '*.png' { 12 | const content: string; 13 | export default content; 14 | } 15 | 16 | declare module '*.module.css' { 17 | const content: Record; 18 | export default content; 19 | } 20 | -------------------------------------------------------------------------------- /sdks/js/packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/close-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/close-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/coin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/sdks/js/packages/core/react/assets/logo.png -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/organization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/sdks/js/packages/core/react/assets/organization.png -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/resize-collapse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/resize-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/resize-expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/sdks/js/packages/core/react/assets/user.png -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Headline, Flex } from '@raystack/apsara/v1'; 2 | import React, { ComponentPropsWithRef } from 'react'; 3 | import logo from '~/react/assets/logo.png'; 4 | 5 | // @ts-ignore 6 | import styles from './header.module.css'; 7 | 8 | const defaultLogo = ( 9 | // eslint-disable-next-line @next/next/no-img-element 10 | logo 15 | ); 16 | 17 | type HeaderProps = ComponentPropsWithRef<'div'> & { 18 | title?: string; 19 | logo?: React.ReactNode; 20 | }; 21 | 22 | export const Header = ({ title, logo }: HeaderProps) => { 23 | return ( 24 | 30 |
{logo ? logo : defaultLogo}
31 |
32 | {title} 33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Text } from '@raystack/apsara/v1'; 2 | import { PropsWithChildren } from 'react'; 3 | import styles from './layout.module.css'; 4 | 5 | interface LayoutProps { 6 | title: string; 7 | } 8 | 9 | export function Layout({ title, children }: PropsWithChildren) { 10 | return ( 11 | 12 | 13 | {title} 14 | 15 | 16 | {children} 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/Layout/layout.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | padding: var(--rs-space-5) var(--rs-space-11); 3 | border-bottom: 1px solid var(--rs-color-border-base-primary); 4 | gap: var(--rs-space-3); 5 | } 6 | 7 | .container { 8 | padding: var(--rs-space-9) var(--rs-space-11); 9 | overflow: scroll; 10 | } 11 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/common/upcoming-plan-change-banner/styles.module.css: -------------------------------------------------------------------------------- 1 | .changeBannerBox { 2 | padding: var(--rs-space-3); 3 | border-radius: var(--rs-space-2); 4 | border: 0.5px solid var(--rs-color-border-accent-primary); 5 | background: var(--rs-color-background-accent-primary); 6 | } 7 | 8 | .flex1 { 9 | flex: 1; 10 | } 11 | 12 | .currentPlanInfoText { 13 | color: var(--rs-color-foreground-base-primary); 14 | font-weight: 400; 15 | } 16 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/container.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-width: 220px; 3 | max-width: 480px; 4 | width: 100%; 5 | color: var(--rs-color-foreground-base-primary); 6 | } 7 | 8 | .title { 9 | font-weight: 400; 10 | } 11 | 12 | .shadowxs { 13 | box-shadow: var(--rs-shadow-feather); 14 | } 15 | .shadowsm { 16 | box-shadow: var(--rs-shadow-soft); 17 | } 18 | .shadowmd { 19 | box-shadow: var(--rs-shadow-lifted); 20 | } 21 | .shadowlg { 22 | box-shadow: var(--rs-shadow-floating); 23 | } 24 | 25 | .radiusxs { 26 | border-radius: var(--rs-radius-2); 27 | } 28 | .radiussm { 29 | border-radius: var(--rs-radius-4); 30 | } 31 | .radiusmd { 32 | border-radius: var(--rs-radius-6); 33 | } 34 | .radiuslg { 35 | border-radius: 24px; 36 | } -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/header.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-width: 220px; 3 | max-width: 100%; 4 | color: var(--rs-color-foreground-base-primary); 5 | } 6 | 7 | .title { 8 | font-weight: 400; 9 | } -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './onboarding/signin'; 2 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/billing/invoices/invoice.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | margin-top: calc(-1 * var(--rs-space-9)); 3 | padding-top: var(--rs-space-9); 4 | } 5 | 6 | .linkColumn { 7 | text-align: right; 8 | } 9 | 10 | .linkColumn > a { 11 | color: var(--rs-color-foreground-accent-primary); 12 | text-decoration: none; 13 | } 14 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/domain/domain.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | padding: var(--rs-space-9) var(--rs-space-11); 4 | overflow: hidden; 5 | } 6 | 7 | .header { 8 | padding: var(--rs-space-5) var(--rs-space-11); 9 | border-bottom: 1px solid var(--rs-color-border-base-primary); 10 | gap: var(--rs-space-3); 11 | } 12 | 13 | .tableWrapper { 14 | height: 100%; 15 | } 16 | 17 | .tableRoot { 18 | height: 100%; 19 | overflow-y: auto; 20 | overflow-x: hidden; 21 | } 22 | 23 | .tableHeader { 24 | z-index: 1; 25 | } 26 | 27 | .tableSearchWrapper { 28 | max-width: 500px; 29 | flex: 1; 30 | } 31 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/members/member.types.tsx: -------------------------------------------------------------------------------- 1 | import { MemberWithInvite } from '~/react/hooks/useOrganizationMembers'; 2 | import { V1Beta1User, V1Beta1Role } from '~/src'; 3 | 4 | export type MembersType = { 5 | users: V1Beta1User[]; 6 | }; 7 | 8 | export enum MemberActionmethods { 9 | InviteMember = 'invite' 10 | } 11 | 12 | export type MembersTableType = { 13 | isLoading?: boolean; 14 | users: MemberWithInvite[]; 15 | organizationId: string; 16 | canCreateInvite?: boolean; 17 | canDeleteUser?: boolean; 18 | memberRoles: Record; 19 | roles: V1Beta1Role[]; 20 | refetch?: () => void; 21 | }; 22 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/members/members.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | padding: var(--rs-space-9) var(--rs-space-11); 4 | overflow: hidden; 5 | } 6 | 7 | .header { 8 | padding: var(--rs-space-5) var(--rs-space-11); 9 | border-bottom: 1px solid var(--rs-color-border-base-primary); 10 | gap: var(--rs-space-3); 11 | } 12 | 13 | .tableWrapper { 14 | height: 100%; 15 | } 16 | 17 | .tableRoot { 18 | height: calc(100% - 128px); 19 | overflow-y: auto; 20 | overflow-x: hidden; 21 | } 22 | 23 | .tableHeader { 24 | z-index: 1; 25 | } 26 | 27 | .tableSearchWrapper { 28 | max-width: 500px; 29 | flex: 1; 30 | } 31 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/organization.module.css: -------------------------------------------------------------------------------- 1 | .createContainer { 2 | margin: auto; 3 | width: 100%; 4 | max-width: 384px; 5 | padding: var(--rs-space-9); 6 | } 7 | 8 | .overlay { 9 | z-index: var(--rs-z-index-portal); 10 | background-color: rgba(104, 112, 118, 0.5); 11 | } 12 | 13 | .dropdownActionItem { 14 | cursor: pointer; 15 | gap: var(--rs-space-3); 16 | display: flex; 17 | align-items: center; 18 | text-decoration: none; 19 | color: var(--rs-color-foreground-base-primary); 20 | background: var(--rs-color-background-base-primary); 21 | padding: var(--rs-space-3); 22 | flex: 1; 23 | } 24 | 25 | .dropdownActionItem:hover { 26 | background: var(--rs-color-background-base-primary-hover); 27 | color: var(--rs-color-foreground-base-primary); 28 | } 29 | 30 | .noSelectItem { 31 | color: var(--rs-color-foreground-base-secondary); 32 | padding: var(--rs-space-3); 33 | } 34 | 35 | .orgTabsContainer { 36 | box-sizing: border-box; 37 | } 38 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/preferences/preferences.types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export type PreferencesSelectionTypes = { 4 | label: string; 5 | name: string; 6 | text: string; 7 | defaultValue?: string; 8 | isLoading?: boolean; 9 | disabled?: boolean; 10 | values: { title: ReactNode; value: string }[]; 11 | onSelection?: (value: string) => void; 12 | }; 13 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/project/project.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | padding: var(--rs-space-9) var(--rs-space-11); 4 | overflow: hidden; 5 | box-sizing: border-box; 6 | } 7 | 8 | .header { 9 | padding: var(--rs-space-5) var(--rs-space-11); 10 | border-bottom: 1px solid var(--rs-color-border-base-primary); 11 | gap: var(--rs-space-3); 12 | } 13 | 14 | .tableWrapper { 15 | height: 100%; 16 | } 17 | 18 | .tableRoot { 19 | height: 100%; 20 | overflow-y: auto; 21 | overflow-x: hidden; 22 | } 23 | 24 | .tableHeader { 25 | z-index: 1; 26 | } 27 | 28 | .tableSearchWrapper { 29 | max-width: 500px; 30 | flex: 1; 31 | } 32 | 33 | .tabContent { 34 | height: 100%; 35 | } 36 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/security/security.types.tsx: -------------------------------------------------------------------------------- 1 | export type SecurityCheckboxTypes = { 2 | label: string; 3 | name: string; 4 | text: string; 5 | value: boolean; 6 | canUpdatePreference?: boolean; 7 | onValueChange: (key: string, checked: boolean) => void; 8 | }; 9 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/sidebar/sidebar.module.css: -------------------------------------------------------------------------------- 1 | .sidebarWrapper { 2 | box-sizing: border-box; 3 | } 4 | 5 | .scrollArea { 6 | padding-right: var(--rs-space-5); 7 | width: 100%; 8 | overflow-y: auto; 9 | } 10 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/styles.ts: -------------------------------------------------------------------------------- 1 | export const styles = { 2 | header: { 3 | padding: 'var(--rs-space-5) var(--rs-space-11)', 4 | borderBottom: '1px solid var(--rs-color-border-base-primary)', 5 | gap: 'var(--rs-space-3)' 6 | }, 7 | container: { 8 | padding: '32px 48px', 9 | overflow: 'scroll' 10 | }, 11 | general: {} 12 | }; 13 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/teams/teams.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | padding: var(--rs-space-9) var(--rs-space-11); 4 | overflow: hidden; 5 | box-sizing: border-box; 6 | } 7 | 8 | .header { 9 | padding: var(--rs-space-5) var(--rs-space-11); 10 | border-bottom: 1px solid var(--rs-color-border-base-primary); 11 | gap: var(--rs-space-3); 12 | } 13 | 14 | .tableWrapper { 15 | height: 100%; 16 | } 17 | 18 | .tableRoot { 19 | height: 100%; 20 | overflow-y: auto; 21 | overflow-x: hidden; 22 | } 23 | 24 | .tableHeader { 25 | z-index: 1; 26 | } 27 | 28 | .tableSearchWrapper { 29 | max-width: 500px; 30 | flex: 1; 31 | } 32 | 33 | .tabContent { 34 | height: 100%; 35 | } 36 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/tokens/token.module.css: -------------------------------------------------------------------------------- 1 | .flex1 { 2 | flex: 1; 3 | } 4 | 5 | .coinIcon { 6 | height: 48px; 7 | width: 48px; 8 | } 9 | 10 | .balancePanel { 11 | padding: var(--rs-space-5); 12 | border-radius: var(--rs-space-2); 13 | border: 1px solid var(--rs-color-border-base-secondary); 14 | } 15 | 16 | .balanceTokenBox { 17 | gap: var(--rs-space-4); 18 | } 19 | 20 | .textMuted { 21 | color: var(--rs-color-foreground-base-secondary); 22 | } 23 | 24 | .addTokenButton { 25 | height: fit-content; 26 | } 27 | 28 | .txnTableHeader { 29 | z-index: 1; 30 | } 31 | 32 | .txnTableEventColumn { 33 | /* max-width: 200px; */ 34 | text-wrap: inherit; 35 | } 36 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/organization/user/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Text, Flex } from '@raystack/apsara/v1'; 4 | import { styles } from '../styles'; 5 | import { UpdateProfile } from './update'; 6 | 7 | export function UserSetting() { 8 | return ( 9 | 10 | 11 | Profile 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/components/window/window.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0; 3 | } 4 | 5 | .dialogContentZoomin { 6 | width: 100vw; 7 | height: 100vh; 8 | max-height: unset; 9 | margin-top: 0; 10 | zindex: 1; 11 | } 12 | 13 | .dialogContentZoomout { 14 | width: 80vw; 15 | height: 80vh; 16 | } 17 | 18 | .overlay { 19 | background-color: rgba(104, 112, 118, 0.50); 20 | } -------------------------------------------------------------------------------- /sdks/js/packages/core/react/hooks/useCopyToClipboard.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | type CopyFn = (text: string) => Promise; // Return success 4 | 5 | export const useCopyToClipboard = () => { 6 | const [copiedText, setCopiedText] = useState(''); 7 | 8 | const copy: CopyFn = async text => { 9 | if (!navigator?.clipboard) { 10 | console.warn('Clipboard not supported'); 11 | return false; 12 | } 13 | 14 | // Try to save to clipboard then save it in the state if worked 15 | try { 16 | await navigator.clipboard.writeText(text); 17 | setCopiedText(text); 18 | return true; 19 | } catch (error) { 20 | console.warn('Copy failed', error); 21 | setCopiedText(''); 22 | return false; 23 | } 24 | }; 25 | 26 | return { copiedText, copy }; 27 | }; 28 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./dist/index.js", 3 | "module": "./dist/index.mjs", 4 | "types": "./dist/index.d.ts", 5 | "exports": "./dist/index.mjs", 6 | "private": true, 7 | "peerDependencies": { 8 | "react": "*" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /sdks/js/packages/core/react/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_DATE_FORMAT = 'DD MMM YYYY'; 2 | export const DEFAULT_DATE_SHORT_FORMAT = 'DD MMM'; 3 | export const DEFAULT_TOKEN_PRODUCT_NAME = 'token'; 4 | export const DEFAULT_PLAN_UPGRADE_MESSAGE = 5 | 'Any remaining balance from your current plan will be prorated and credited to your account in future billing cycles.'; 6 | 7 | export const NEGATIVE_BALANCE_TOOLTIP_MESSAGE = 8 | 'This negative amount shows a credit balance for prorated seats, which will be applied to future invoices.'; 9 | 10 | export const SUBSCRIPTION_STATES = { 11 | ACTIVE: 'active', 12 | PAST_DUE: 'past_due', 13 | TRIALING: 'trialing', 14 | CANCELED: 'canceled' 15 | } as const; 16 | 17 | export const INVOICE_STATES = { 18 | OPEN: 'open', 19 | PAID: 'paid', 20 | DRAFT: 'draft' 21 | } as const; 22 | 23 | export const DEFAULT_API_PLATFORM_APP_NAME = 'Frontier'; 24 | 25 | export const PREFERENCE_OPTIONS = { 26 | NEWSLETTER: 'newsletter' 27 | } as const; 28 | -------------------------------------------------------------------------------- /sdks/js/packages/core/scripts/bump-version.js: -------------------------------------------------------------------------------- 1 | const semver = require("semver") 2 | const fs = require("fs/promises") 3 | const path = require("path") 4 | 5 | const pkg = require("../package.json") 6 | 7 | async function updatePackageVersion() { 8 | try { 9 | const gitRef = process.env.GIT_REFNAME 10 | const gitTag = semver.valid(gitRef); 11 | if (gitTag && semver.compare(gitTag, pkg.version) > 0) { 12 | pkg.version = gitTag; 13 | console.log('Updating JS SDK version to', gitTag) 14 | await fs.writeFile(path.join(process.cwd(), 'package.json'), JSON.stringify(pkg, null, 2)) 15 | } 16 | } catch (err) { 17 | console.error("Update Package Version", err) 18 | } 19 | } 20 | 21 | updatePackageVersion() -------------------------------------------------------------------------------- /sdks/js/packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { V1Beta1 } from '../api-client/V1Beta1'; 2 | 3 | export * from '../api-client/V1Beta1'; 4 | export * from '../api-client/data-contracts'; 5 | export const FrontierClient = V1Beta1; 6 | -------------------------------------------------------------------------------- /sdks/js/packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@raystack/frontier-tsconfig/react-library.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "~/*": ["./*"] 6 | }, 7 | "types": ["jest"] 8 | }, 9 | "include": ["."], 10 | "exclude": ["dist", "react/dist", "node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /sdks/js/packages/core/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import cssModulesPlugin from 'esbuild-css-modules-plugin'; 2 | import { defineConfig } from 'tsup'; 3 | 4 | export default defineConfig(() => [ 5 | // Core API 6 | { 7 | entry: ['src/index.ts'], 8 | format: ['cjs', 'esm'], 9 | dts: true 10 | }, 11 | // React APIs 12 | { 13 | entry: ['react/index.ts'], 14 | outDir: 'react/dist', 15 | banner: { 16 | js: "'use client'" 17 | }, 18 | format: ['cjs', 'esm'], 19 | external: ['react', 'svelte', 'vue', 'solid-js', 'api-client/*'], 20 | dts: true, 21 | loader: { 22 | '.svg': 'dataurl', 23 | '.png': 'dataurl' 24 | }, 25 | esbuildPlugins: [cssModulesPlugin()] 26 | }, 27 | { 28 | entry: ['api-client/index.ts'], 29 | format: ['cjs', 'esm'], 30 | outDir: 'api-client/dist', 31 | dts: true, 32 | }, 33 | ]); 34 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | .env 4 | 5 | .npmrc -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/.eslintignore: -------------------------------------------------------------------------------- 1 | .next 2 | .nuxt 3 | node_modules 4 | dist -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@raystack/eslint-config'], 4 | rules: { 5 | 'turbo/no-undeclared-env-vars': 'off' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/.npmrc: -------------------------------------------------------------------------------- 1 | prefer-workspace-packages=true -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-slim AS base 2 | ENV PNPM_HOME="/pnpm" 3 | ENV PATH="$PNPM_HOME:$PATH" 4 | RUN corepack enable 5 | 6 | WORKDIR /app 7 | 8 | COPY package.json . 9 | RUN npm install 10 | COPY . . 11 | RUN npm run build 12 | EXPOSE 3000 13 | ENV PORT 3000 14 | 15 | COPY ./.next/standalone build/. 16 | COPY ./.next/static build/.next/static/. 17 | CMD ["node", "build/server.js"] 18 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const nextConfig = { 4 | output: "standalone", 5 | }; 6 | 7 | export default nextConfig; -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sdk-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@raystack/apsara": "0.46.0-rc.5", 13 | "@raystack/frontier": "^0.65.0", 14 | "next": "14.2.5", 15 | "next-http-proxy-middleware": "^1.2.6", 16 | "react": "^18", 17 | "react-dom": "^18", 18 | "uuid": "^10.0.0" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^20", 22 | "@types/react": "^18", 23 | "@types/react-dom": "^18", 24 | "@types/uuid": "^10.0.0", 25 | "typescript": "^5" 26 | }, 27 | "packageManager": "pnpm@8.6.10" 28 | } -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/api/frontier.ts: -------------------------------------------------------------------------------- 1 | import { ApiClient } from '@raystack/frontier/api-client' 2 | import config from '@/config/frontier'; 3 | 4 | const client = new ApiClient({ 5 | baseUrl: config.endpoint, 6 | baseApiParams: { 7 | credentials: 'include' 8 | } 9 | }); 10 | 11 | export default client -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import config from '@/config/frontier'; 3 | import AuthContextProvider from '@/contexts/auth/provider'; 4 | import { customFetch } from '@/utils/custom-fetch'; 5 | import { FrontierProvider } from '@raystack/frontier/react'; 6 | import type React from 'react'; 7 | 8 | export default function RootLayout({ 9 | children 10 | }: Readonly<{ 11 | children: React.ReactNode; 12 | }>) { 13 | return ( 14 | 15 | 16 | 17 | {children} 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import useAuthRedirect from '@/hooks/useAuthRedirect'; 4 | import { Flex } from '@raystack/apsara/v1'; 5 | import { SignIn } from '@raystack/frontier/react'; 6 | 7 | export default function LoginRoute() { 8 | useAuthRedirect(); 9 | return ( 10 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/app/magiclink-verify/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Flex } from '@raystack/apsara/v1'; 4 | import { MagicLinkVerify } from '@raystack/frontier/react'; 5 | import React from 'react'; 6 | 7 | export default function LoginRoute() { 8 | return ( 9 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/app/organizations/[orgId]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Window, OrganizationProfile } from '@raystack/frontier/react'; 3 | 4 | export default function OrgPage({ params }: { params: { orgId: string } }) { 5 | const orgId = params.orgId; 6 | 7 | return orgId ? ( 8 | {}}> 9 | 15 | 16 | ) : null; 17 | } 18 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/app/signup/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import useAuthRedirect from '@/hooks/useAuthRedirect'; 3 | import { Flex } from '@raystack/apsara/v1'; 4 | import { SignUp } from '@raystack/frontier/react'; 5 | 6 | export default function SignUpRoute() { 7 | useAuthRedirect(); 8 | 9 | return ( 10 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/app/subscribe/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Flex } from '@raystack/apsara/v1'; 3 | // import { Subscribe } from '@raystack/frontier/react'; 4 | import React from 'react'; 5 | export default function SubscribeRoute() { 6 | return ( 7 | 13 | {/* console.log(JSON.stringify(data))} 15 | /> */} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/app/updates/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Flex } from '@raystack/apsara/v1'; 3 | // import { Updates } from '@raystack/frontier/react'; 4 | import React from 'react'; 5 | 6 | export default function SubscribeRoute() { 7 | return ( 8 | 13 | {/* alert(JSON.stringify(data))} /> */} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/config/frontier.ts: -------------------------------------------------------------------------------- 1 | import type { FrontierClientOptions } from '@raystack/frontier/react'; 2 | 3 | const config: FrontierClientOptions = { 4 | endpoint: '/api', 5 | billing: { 6 | basePlan: { 7 | title: 'Standard Plan' 8 | } 9 | } 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/contexts/auth/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { createContext } from 'react'; 3 | 4 | interface AuthContextType { 5 | isAuthorized: boolean; 6 | setIsAuthorized: (v: boolean) => void; 7 | } 8 | 9 | const AuthContext = createContext({ 10 | isAuthorized: false, 11 | setIsAuthorized: () => {} 12 | }); 13 | 14 | export default AuthContext; 15 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/contexts/auth/provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { PropsWithChildren, useEffect, useState } from 'react'; 4 | import AuthContext from '.'; 5 | import { useFrontier } from '@raystack/frontier/react'; 6 | 7 | const AuthContextProvider: React.FC> = ({ children }) => { 8 | const [isAuthorized, setIsAuthorized] = useState(false); 9 | 10 | const { user, isUserLoading } = useFrontier(); 11 | 12 | useEffect(() => { 13 | if (user?.id) { 14 | setIsAuthorized(true); 15 | } else if (!user?.id && !isUserLoading) { 16 | setIsAuthorized(false); 17 | } 18 | }, [user?.id, isUserLoading]); 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | export default AuthContextProvider; 28 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/hooks/useAuthRedirect.tsx: -------------------------------------------------------------------------------- 1 | import AuthContext from '@/contexts/auth'; 2 | import { redirect } from 'next/navigation'; 3 | import { useContext, useEffect } from 'react'; 4 | 5 | const useAuthRedirect = () => { 6 | const { isAuthorized } = useContext(AuthContext); 7 | 8 | useEffect(() => { 9 | if (isAuthorized) { 10 | redirect('/'); 11 | } 12 | }, [isAuthorized]); 13 | }; 14 | 15 | export default useAuthRedirect; 16 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/pages/api/[...all].ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import httpProxyMiddleware from 'next-http-proxy-middleware'; 3 | 4 | export const config = { 5 | api: { 6 | // Enable `externalResolver` option in Next.js 7 | externalResolver: true 8 | } 9 | }; 10 | 11 | // eslint-disable-next-line import/no-anonymous-default-export 12 | export default async function handler( 13 | req: NextApiRequest, 14 | res: NextApiResponse 15 | ) { 16 | const baseUrl = process.env.FRONTIER_ENDPOINT || 'http://frontier:8080'; 17 | 18 | await httpProxyMiddleware(req, res, { 19 | // You can use the `http-proxy` option 20 | target: baseUrl, 21 | pathRewrite: [ 22 | { 23 | patternStr: '^/api', 24 | replaceStr: '' 25 | } 26 | ] 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/src/utils/custom-fetch.ts: -------------------------------------------------------------------------------- 1 | import type { V1Beta1Organization } from '@raystack/frontier'; 2 | import { v4 as uuid } from 'uuid'; 3 | 4 | export const customFetch = (activeOrg?: V1Beta1Organization) => { 5 | return (...fetchParams: Parameters) => { 6 | const [url, opts] = fetchParams; 7 | return fetch(url, { 8 | ...opts, 9 | headers: { 10 | ...opts?.headers, 11 | 'X-Request-Id': uuid(), 12 | 'X-Frontier-Org-Id': activeOrg?.id || '' 13 | } 14 | }); 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /sdks/js/packages/sdk-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /sdks/js/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "tools/*" 4 | -------------------------------------------------------------------------------- /sdks/js/tools/eslint-config/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable strict */ 2 | module.exports = { 3 | plugins: ["test-selectors"], 4 | extends: [ 5 | 'next', 6 | 'turbo', 7 | 'prettier', 8 | "eslint:recommended", 9 | 'plugin:test-selectors/recommended' 10 | ], 11 | rules: { 12 | '@next/next/no-html-link-for-pages': 'off', 13 | "no-unused-vars": "warn", 14 | "@next/next/no-img-element": "off" 15 | }, 16 | parserOptions: { 17 | babelOptions: { 18 | presets: [require.resolve('next/babel')] 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /sdks/js/tools/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@raystack/eslint-config", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "private": true, 7 | "dependencies": { 8 | "eslint-config-next": "^13.4.1", 9 | "eslint-config-prettier": "^8.3.0", 10 | "eslint-config-turbo": "^1.9.3", 11 | "eslint-plugin-react": "7.28.0", 12 | "eslint-plugin-react-hooks": "^4.6.2", 13 | "eslint-plugin-test-selectors": "^2.1.1", 14 | "next": "^13.4.12" 15 | }, 16 | "publishConfig": { 17 | "access": "public" 18 | } 19 | } -------------------------------------------------------------------------------- /sdks/js/tools/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "types": ["@types/node"] 19 | }, 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /sdks/js/tools/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "allowJs": true, 7 | "declaration": false, 8 | "declarationMap": false, 9 | "incremental": true, 10 | "jsx": "preserve", 11 | "lib": ["dom", "dom.iterable", "esnext"], 12 | "module": "esnext", 13 | "noEmit": true, 14 | "resolveJsonModule": true, 15 | "target": "es5" 16 | }, 17 | "include": ["src", "next-env.d.ts"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /sdks/js/tools/tsconfig/node14.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Node 14", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "lib": ["ES2020"], 7 | "module": "commonjs", 8 | "target": "ES2020" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /sdks/js/tools/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@raystack/frontier-tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sdks/js/tools/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "lib": [ 8 | "dom", 9 | "ES2017", 10 | ], 11 | "module": "ESNext", 12 | "target": "es6", 13 | } 14 | } -------------------------------------------------------------------------------- /sdks/js/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": [".next/**", "!.next/cache/**", "dist/**"] 7 | }, 8 | "lint": { 9 | "dependsOn": ["^build", "build"] 10 | }, 11 | "clean": { 12 | "dependsOn": ["^clean"] 13 | }, 14 | "release": { 15 | "dependsOn": ["^build"] 16 | }, 17 | "release:ci": { 18 | "dependsOn": ["^build"] 19 | }, 20 | "dev": { 21 | "cache": false, 22 | "persistent": true 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/e2e/regression/testdata/.gitignore: -------------------------------------------------------------------------------- 1 | stripe_key.txt -------------------------------------------------------------------------------- /test/e2e/regression/testdata/cassettes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/test/e2e/regression/testdata/cassettes/.gitkeep -------------------------------------------------------------------------------- /test/e2e/regression/testdata/resource/compute.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - name: delete 3 | namespace: compute/order 4 | - name: update 5 | namespace: compute/order 6 | - name: get 7 | namespace: compute/order 8 | - name: create 9 | namespace: compute/order 10 | - key: compute.order.configure 11 | - name: get 12 | namespace: compute/disk 13 | - name: create 14 | namespace: compute/disk 15 | - name: delete 16 | namespace: compute/disk 17 | roles: 18 | - name: compute_order_manager 19 | permissions: 20 | - compute_order_delete 21 | - compute_order_update 22 | - compute_order_get 23 | - compute_order_create 24 | - name: compute_order_viewer 25 | permissions: 26 | - compute_order_get 27 | - name: compute_order_owner 28 | permissions: 29 | - compute_order_delete 30 | - compute_order_update 31 | - compute_order_get 32 | - compute_order_create 33 | 34 | -------------------------------------------------------------------------------- /test/e2e/smoke/testdata/resource/potato.yaml: -------------------------------------------------------------------------------- 1 | permissions: 2 | - name: delete 3 | namespace: potato/cart 4 | - name: update 5 | namespace: potato/cart 6 | - name: get 7 | namespace: potato/cart -------------------------------------------------------------------------------- /test/e2e/testbench/frontier.go: -------------------------------------------------------------------------------- 1 | package testbench 2 | 3 | import ( 4 | "github.com/raystack/frontier/cmd" 5 | "github.com/raystack/frontier/config" 6 | "github.com/raystack/salt/log" 7 | ) 8 | 9 | func MigrateFrontier(logger *log.Zap, appConfig *config.Frontier) error { 10 | return cmd.RunMigrations(logger, appConfig.DB) 11 | } 12 | 13 | func StartFrontier(logger *log.Zap, appConfig *config.Frontier) { 14 | go func() { 15 | if err := cmd.StartServer(logger, appConfig); err != nil { 16 | logger.Fatal("err starting", "err", err) 17 | panic(err) 18 | } 19 | }() 20 | } 21 | -------------------------------------------------------------------------------- /test/e2e/testbench/testdata/mocks/mock-group.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "org1-group1" 4 | }, 5 | { 6 | "name": "org1-group2" 7 | }, 8 | { 9 | "name": "org1-group3" 10 | } 11 | ] -------------------------------------------------------------------------------- /test/e2e/testbench/testdata/mocks/mock-project.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "project-1", 4 | "title": "project 1", 5 | "metadata": { 6 | "label": {}, 7 | "description": "description" 8 | } 9 | }, 10 | { 11 | "name": "proj-2" 12 | } 13 | ] -------------------------------------------------------------------------------- /ui/.env.example: -------------------------------------------------------------------------------- 1 | FRONTIER_API_URL=http://localhost:8000 -------------------------------------------------------------------------------- /ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['../sdks/js/tools/eslint-config'], 3 | }; -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist/* 12 | !dist/.gitkeep 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /ui/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: dist test 2 | 3 | build: dep dist 4 | 5 | dist: 6 | @npm run build 7 | 8 | test: 9 | @npm run test 10 | 11 | dep: 12 | @npm install --legacy-peer-deps -f 13 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | # copy .env.example into .env file 9 | # update .env file with FRONTIER_API_URL 10 | npm run dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | ## Learn More 15 | 16 | To learn more about Next.js, take a look at the following resources: 17 | 18 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 19 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 20 | 21 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! -------------------------------------------------------------------------------- /ui/dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raystack/frontier/11e850a108f9ac154b9eb605bdd918ed8767ea56/ui/dist/.gitkeep -------------------------------------------------------------------------------- /ui/embed.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed all:dist 8 | var Assets embed.FS 9 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Frontier 8 | 9 | 10 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /ui/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/public/users.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/scripts/gen-swagger-client.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { generateApi } from "swagger-typescript-api"; 3 | 4 | const cwd = process.cwd(); 5 | const OUTPUT_PATH = path.resolve(cwd, "src", "api"); 6 | // {root}/proto/apidocs.swagger.yaml 7 | const INPUT_PATH = path.resolve(cwd, "..", "proto", "apidocs.swagger.yaml"); 8 | 9 | async function main() { 10 | try { 11 | await generateApi({ 12 | fileName: "frontier.ts", 13 | output: OUTPUT_PATH, 14 | input: INPUT_PATH, 15 | httpClientType: "axios", 16 | }); 17 | } catch (error) { 18 | console.error("Error generating API:", error); 19 | } 20 | } 21 | 22 | main(); 23 | -------------------------------------------------------------------------------- /ui/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 'Inter', sans-serif; 4 | font-size: 12px 5 | } 6 | 7 | @keyframes logo-spin { 8 | from { 9 | transform: rotate(0deg); 10 | } 11 | to { 12 | transform: rotate(360deg); 13 | } 14 | } 15 | 16 | @media (prefers-reduced-motion: no-preference) { 17 | a:nth-of-type(2) .logo { 18 | animation: logo-spin infinite 20s linear; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Flex } from "@raystack/apsara/v1"; 2 | import { Outlet } from "react-router-dom"; 3 | import "@raystack/apsara/style.css"; 4 | import "@raystack/apsara/normalize.css"; 5 | import "./App.css"; 6 | import IAMSidebar from "./components/Sidebar"; 7 | 8 | function App() { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /ui/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Api as FrontierApi } from "./frontier"; 2 | 3 | import { frontierConfig } from "../configs/frontier"; 4 | 5 | const frontierApiInstance = new FrontierApi({ 6 | baseURL: frontierConfig.endpoint, 7 | withCredentials: true, 8 | headers: { 9 | "Content-Type": "application/json", 10 | }, 11 | }); 12 | 13 | export const api = frontierApiInstance.v1Beta1; 14 | -------------------------------------------------------------------------------- /ui/src/assets/icons/admins.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/assets/icons/coin-colored.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ui/src/assets/icons/coin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/assets/icons/cpu-chip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/src/assets/icons/invoices.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/assets/icons/plans.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ui/src/assets/icons/products.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/src/assets/icons/webhooks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/components/DialogTable.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DataTable } from "@raystack/apsara"; 3 | import { ColumnDef } from "@tanstack/table-core"; 4 | import { Flex } from "@raystack/apsara/v1"; 5 | 6 | type DialogTableProps = { 7 | columns: ColumnDef[]; 8 | data: any[]; 9 | header?: React.ReactNode; 10 | }; 11 | export default function DialogTable({ 12 | columns, 13 | data, 14 | header, 15 | }: DialogTableProps) { 16 | return ( 17 | 21 | 27 | {header && {header}} 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/components/Price.tsx: -------------------------------------------------------------------------------- 1 | import { Flex } from "@raystack/apsara/v1"; 2 | import { getCurrencyValue } from "~/utils/helper"; 3 | 4 | type PriceProps = { 5 | value?: string; 6 | currency?: string; 7 | }; 8 | export const Price = ({ value = "", currency = "usd" }: PriceProps) => { 9 | const [intValue, decimalValue, symbol] = getCurrencyValue(value, currency); 10 | return ( 11 | 12 | {symbol} 13 | 14 | {intValue} 15 | .{decimalValue} 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /ui/src/components/Sidebar/sidebar.module.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | padding-top: var(--rs-space-5); 3 | padding-bottom: var(--rs-space-5); 4 | height: auto; 5 | /* TODO: fix width in sidebar component */ 6 | width: 220px !important; 7 | } 8 | 9 | .sidebar-group { 10 | margin-top: var(--rs-space-5); 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/components/TableLoader/index.tsx: -------------------------------------------------------------------------------- 1 | import { Table } from "@raystack/apsara"; 2 | import Skeleton from "react-loading-skeleton"; 3 | 4 | interface TableLoaderProps { 5 | row?: number; 6 | cell?: number; 7 | cellClassName?: string; 8 | } 9 | 10 | export default function TableLoader({ 11 | row = 5, 12 | cell = 3, 13 | cellClassName = "", 14 | }: TableLoaderProps) { 15 | return ( 16 | <> 17 | {[...new Array(row)].map((_, i) => ( 18 | 19 | {[...new Array(cell)].map((_, j) => ( 20 | 21 | 22 | 23 | ))} 24 | 25 | ))} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/components/dialog/header.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Image, Text } from "@raystack/apsara/v1"; 2 | 3 | type DialogHeaderProps = { 4 | title?: string; 5 | }; 6 | export function DialogHeader({ title }: DialogHeaderProps) { 7 | return ( 8 | 13 | {title} 14 | share-image 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/components/page-header.module.css: -------------------------------------------------------------------------------- 1 | .breadcrumb { 2 | list-style: none; 3 | padding: 0; 4 | margin: 0; 5 | display: flex; 6 | font-size: 16px; 7 | } 8 | 9 | 10 | 11 | .breadcrumb a { 12 | text-decoration: none; 13 | color: #007bff; 14 | display: inline-block; 15 | margin-right: 5px; 16 | } 17 | 18 | .breadcrumb a::before { 19 | content: '>'; 20 | color: #666; 21 | } 22 | 23 | .breadcrumb a:first-child::before { 24 | content: ''; /* Remove the arrow before the first element */ 25 | } 26 | 27 | .breadcrumb a:last-child { 28 | margin-right: 0; 29 | } -------------------------------------------------------------------------------- /ui/src/components/page-title/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { AppContext } from "~/contexts/App"; 3 | import { defaultConfig } from "~/utils/constants"; 4 | 5 | interface PageTitleProps { 6 | title?: string; 7 | appName?: string; 8 | } 9 | 10 | export default function PageTitle({ title, appName }: PageTitleProps) { 11 | const { config } = useContext(AppContext); 12 | const titleAppName = appName || config?.title || defaultConfig?.title; 13 | const fullTitle = title ? `${title} | ${titleAppName}` : titleAppName; 14 | 15 | useEffect(() => { 16 | document.title = fullTitle; 17 | 18 | return () => { 19 | document.title = titleAppName; 20 | }; 21 | }, [fullTitle, titleAppName]); 22 | return null; 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/components/sheet/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Flex } from "@raystack/apsara/v1"; 3 | import { CSS } from "@stitches/react"; 4 | 5 | type SheetFooterProps = { 6 | children?: React.ReactNode; 7 | css?: CSS; 8 | }; 9 | 10 | export function SheetFooter({ children, css }: SheetFooterProps) { 11 | // @ts-ignore 12 | return {children}; 13 | } 14 | 15 | const styles = { 16 | footer: { 17 | bottom: 0, 18 | left: 0, 19 | right: 0, 20 | position: "absolute", 21 | justifyContent: "space-between", 22 | padding: "18px 32px", 23 | borderTop: "1px solid $gray4", 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /ui/src/components/sheet/header.tsx: -------------------------------------------------------------------------------- 1 | import { Cross1Icon } from "@radix-ui/react-icons"; 2 | import { Flex, Text } from "@raystack/apsara/v1"; 3 | 4 | type SheetHeaderProps = { 5 | title: string; 6 | onClick: () => void; 7 | }; 8 | 9 | export function SheetHeader({ title, onClick }: SheetHeaderProps) { 10 | return ( 11 | 12 | 13 | {title} 14 | 15 | 20 | 21 | ); 22 | } 23 | 24 | const styles = { 25 | header: { 26 | padding: "18px 32px", 27 | borderBottom: "1px solid var(--border-base)", 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /ui/src/components/states/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Spinner } from "@raystack/apsara/v1"; 2 | 3 | export default function LoadingState() { 4 | return ( 5 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/components/states/Unauthorized.tsx: -------------------------------------------------------------------------------- 1 | import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; 2 | import { Button, EmptyState, Flex } from "@raystack/apsara/v1"; 3 | import { api } from "~/api"; 4 | 5 | export default function UnauthorizedState() { 6 | async function logout() { 7 | await api?.frontierServiceAuthLogout(); 8 | window.location.href = "/"; 9 | window.location.reload(); 10 | } 11 | return ( 12 | 13 | } 15 | heading="Unauthorized" 16 | subHeading="You dont have access to view this page" 17 | primaryAction={ 18 | 24 | } 25 | > 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/configs/frontier.tsx: -------------------------------------------------------------------------------- 1 | const getFrontierConfig = () => { 2 | const frontierEndpoint = 3 | process.env.NEXT_PUBLIC_FRONTIER_URL || "/frontier-api"; 4 | const currentHost = window?.location?.origin || "http://localhost:3000"; 5 | return { 6 | endpoint: frontierEndpoint, 7 | redirectLogin: `${currentHost}/login`, 8 | redirectSignup: `${currentHost}/signup`, 9 | redirectMagicLinkVerify: `${currentHost}/magiclink-verify`, 10 | callbackUrl: `${currentHost}/callback`, 11 | }; 12 | }; 13 | 14 | export const frontierConfig = getFrontierConfig(); 15 | -------------------------------------------------------------------------------- /ui/src/configs/theme.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProviderProps } from "@raystack/apsara/v1"; 2 | 3 | export const themeConfig: ThemeProviderProps = { 4 | defaultTheme: "system", 5 | grayColor: "gray", 6 | }; 7 | -------------------------------------------------------------------------------- /ui/src/containers/billingplans.list/header.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable, useTable } from "@raystack/apsara"; 2 | import PageHeader from "~/components/page-header"; 3 | 4 | export const PlanHeader = ({ header }: any) => { 5 | const { filteredColumns } = useTable(); 6 | const isFiltered = filteredColumns.length > 0; 7 | 8 | return ( 9 | 10 | {isFiltered ? : } 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /ui/src/containers/groups.list/types.tsx: -------------------------------------------------------------------------------- 1 | import { V1Beta1Group } from "@raystack/frontier"; 2 | 3 | export type GroupDetailsTypes = { 4 | group: V1Beta1Group; 5 | }; 6 | -------------------------------------------------------------------------------- /ui/src/containers/home.tsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return <>Home; 3 | } 4 | -------------------------------------------------------------------------------- /ui/src/containers/invoices.list/header.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable, useTable } from "@raystack/apsara"; 2 | import PageHeader from "~/components/page-header"; 3 | 4 | export const InvoicesHeader = ({ header }: any) => { 5 | const { filteredColumns } = useTable(); 6 | const isFiltered = filteredColumns.length > 0; 7 | 8 | return ( 9 | 10 | {isFiltered ? : } 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /ui/src/containers/magiclink.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Flex, Image } from "@raystack/apsara/v1"; 4 | import { MagicLinkVerify } from "@raystack/frontier/react"; 5 | import IAMIcon from "~/assets/icons/iam.svg?react"; 6 | import { AppContext } from "~/contexts/App"; 7 | import { useContext } from "react"; 8 | 9 | export default function MagicLink() { 10 | const { config } = useContext(AppContext); 11 | 12 | return ( 13 | 18 | 28 | ) : ( 29 | 30 | ) 31 | } 32 | /> 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/containers/products.create/base-fields.tsx: -------------------------------------------------------------------------------- 1 | import { Flex } from "@raystack/apsara/v1"; 2 | import { UseFormReturn } from "react-hook-form"; 3 | 4 | import { CustomFieldName } from "~/components/CustomField"; 5 | import { ProductForm } from "./contants"; 6 | 7 | export const BaseFields = ({ 8 | methods, 9 | }: { 10 | methods: UseFormReturn; 11 | }) => { 12 | return ( 13 | 14 | 15 | 20 | 26 | 27 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /ui/src/containers/products.create/transform.ts: -------------------------------------------------------------------------------- 1 | import * as R from "ramda"; 2 | const rejectEmptyValue = R.reject(R.anyPass([R.isEmpty, R.isNil])); 3 | 4 | const splitAndTrim = R.pipe( 5 | R.split(","), 6 | R.map(R.trim), 7 | R.filter(R.complement(R.isEmpty)) 8 | ); 9 | 10 | export function updateResponse(data: any) { 11 | data.metadata = (data.metadata || []).reduce((acc: any, metadata: any) => { 12 | acc[metadata.key] = metadata.value; 13 | return acc; 14 | }, {}); 15 | 16 | data.behavior_config = Object.keys(data.behavior_config || {}).reduce( 17 | (acc: Record, key: string) => { 18 | if (data.behavior_config[key]) { 19 | acc[key] = parseInt(data.behavior_config[key] || ""); 20 | } 21 | return acc; 22 | }, 23 | {} 24 | ); 25 | 26 | const newFeatures = splitAndTrim(data.newfeatures || ""); 27 | delete data.newfeatures; 28 | data.features = [...data.features, ...newFeatures.map((f) => ({ name: f }))]; 29 | 30 | return rejectEmptyValue(data); 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/containers/products.edit/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import CreateOrUpdateProduct from "../products.create"; 4 | import { V1Beta1Product } from "@raystack/frontier"; 5 | import { api } from "~/api"; 6 | 7 | export default function EditProduct() { 8 | let { productId } = useParams(); 9 | const [product, setProduct] = useState(); 10 | 11 | useEffect(() => { 12 | async function fetchProduct() { 13 | try { 14 | const res = await api?.frontierServiceGetProduct(productId as string); 15 | const product = res?.data?.product; 16 | setProduct(product); 17 | } catch (error) { 18 | console.error(error); 19 | } 20 | } 21 | 22 | if (productId) fetchProduct(); 23 | }, [productId]); 24 | 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/containers/products.list/types.tsx: -------------------------------------------------------------------------------- 1 | import { V1Beta1Product } from "@raystack/frontier"; 2 | 3 | export type ProductDetailsTypes = { 4 | product: V1Beta1Product; 5 | }; 6 | -------------------------------------------------------------------------------- /ui/src/containers/roles.list/details.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from "@raystack/apsara"; 2 | import { Flex, Text } from "@raystack/apsara/v1"; 3 | import { useRole } from "."; 4 | 5 | export default function RoleDetails() { 6 | const { role } = useRole(); 7 | 8 | return ( 9 | 19 | {role?.name} 20 | 21 | 22 | Name 23 | {role?.name} 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/containers/roles.list/header.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable, useTable } from "@raystack/apsara"; 2 | import PageHeader from "~/components/page-header"; 3 | 4 | const pageHeader = { 5 | title: "Roles", 6 | breadcrumb: [], 7 | }; 8 | 9 | export const RolesHeader = () => { 10 | const { filteredColumns } = useTable(); 11 | const isFiltered = filteredColumns.length > 0; 12 | 13 | return ( 14 | 15 | {isFiltered ? : } 16 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /ui/src/containers/roles.list/types.tsx: -------------------------------------------------------------------------------- 1 | import { V1Beta1Role } from "@raystack/frontier"; 2 | 3 | export type RoleDetailsTypes = { 4 | role: V1Beta1Role; 5 | }; 6 | -------------------------------------------------------------------------------- /ui/src/containers/super_admins/list.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable } from "@raystack/apsara"; 2 | import { Flex } from "@raystack/apsara/v1"; 3 | import { useContext } from "react"; 4 | import { AppContext } from "~/contexts/App"; 5 | import { getColumns } from "./columns"; 6 | 7 | export function SuperAdminList() { 8 | const { platformUsers } = useContext(AppContext); 9 | 10 | const tableStyle = { width: "100%" }; 11 | const columns = getColumns(); 12 | const data = [ 13 | ...(platformUsers?.users || []), 14 | ...(platformUsers?.serviceusers || []), 15 | ]; 16 | return ( 17 | 18 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/containers/users.list/styles.module.css: -------------------------------------------------------------------------------- 1 | .orgsTable { 2 | margin-top: var(--mr-16); 3 | } 4 | 5 | .tableCell { 6 | width: 25%; 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/containers/users.list/types.tsx: -------------------------------------------------------------------------------- 1 | import { V1Beta1User } from "@raystack/frontier"; 2 | 3 | export type UserDetailsTypes = { 4 | user: V1Beta1User; 5 | }; 6 | -------------------------------------------------------------------------------- /ui/src/hooks/useFeatures.ts: -------------------------------------------------------------------------------- 1 | import { V1Beta1Feature } from "@raystack/frontier"; 2 | import { useEffect, useState } from "react"; 3 | import { toast } from "sonner"; 4 | import { api } from "~/api"; 5 | 6 | export function useFeatures() { 7 | const [features, setFeatures] = 8 | useState<{ label: string | undefined; value: string | undefined }[]>(); 9 | 10 | useEffect(() => { 11 | async function getFeatures() { 12 | try { 13 | const res = await api?.frontierServiceListFeatures(); 14 | const features = res?.data?.features ?? []; 15 | setFeatures( 16 | features.map((f: V1Beta1Feature) => ({ 17 | label: f.name, 18 | value: f.name, 19 | })) 20 | ); 21 | } catch (error: any) { 22 | toast.error("Something went wrong", { 23 | description: error.message, 24 | }); 25 | } 26 | } 27 | getFeatures(); 28 | }, []); 29 | 30 | return { features }; 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/layout/auth.tsx: -------------------------------------------------------------------------------- 1 | import { FrontierProvider } from "@raystack/frontier/react"; 2 | import { Outlet } from "react-router-dom"; 3 | import { frontierConfig } from "~/configs/frontier"; 4 | import { themeConfig } from "~/configs/theme"; 5 | 6 | // TODO: remove frontier client dependency from auth pages like login, signup etc in SDK 7 | export default function AuthLayout() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider, ToastContainer } from "@raystack/apsara/v1"; 2 | import { SkeletonTheme } from "react-loading-skeleton"; 3 | import React from "react"; 4 | import ReactDOM from "react-dom/client"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | 7 | import Routes from "./routes"; 8 | import { AppContextProvider } from "./contexts/App"; 9 | import { themeConfig } from "~/configs/theme"; 10 | 11 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | , 27 | ); 28 | -------------------------------------------------------------------------------- /ui/src/pages/organizations/details/apis/apis.module.css: -------------------------------------------------------------------------------- 1 | .empty-state { 2 | height: 100%; 3 | } 4 | 5 | .empty-state-subheading { 6 | max-width: 360px; 7 | text-wrap: auto; 8 | } 9 | 10 | .table { 11 | width: 100%; 12 | table-layout: fixed; 13 | } 14 | 15 | .table-wrapper { 16 | /* Navbar Height + Toolbar height */ 17 | max-height: calc(100vh - 90px); 18 | overflow: scroll; 19 | } 20 | 21 | .table-header { 22 | /* position: relative; */ 23 | z-index: 2; 24 | } 25 | 26 | .first-column { 27 | padding-left: var(--rs-space-7); 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/pages/organizations/details/edit/edit.module.css: -------------------------------------------------------------------------------- 1 | .drawer-content { 2 | min-width: 500px; 3 | padding: 0; 4 | width: fit-content; 5 | } 6 | 7 | .side-panel { 8 | width: 100%; 9 | height: 100%; 10 | display: flex; 11 | flex-direction: column; 12 | overflow: auto; 13 | } 14 | 15 | .side-panel-form { 16 | height: 100%; 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: space-between; 20 | } 21 | 22 | .side-panel-content { 23 | padding: var(--rs-space-7); 24 | } 25 | 26 | .side-panel-footer { 27 | border-top: 1px solid var(--rs-color-border-base-primary); 28 | padding: var(--rs-space-5) var(--rs-space-7); 29 | } 30 | 31 | .select-value { 32 | width: 100%; 33 | } 34 | 35 | .select-content { 36 | max-height: 400px; 37 | } 38 | -------------------------------------------------------------------------------- /ui/src/pages/organizations/details/invoices/invoices.module.css: -------------------------------------------------------------------------------- 1 | .empty-state { 2 | height: 100%; 3 | } 4 | 5 | .empty-state-subheading { 6 | max-width: 360px; 7 | text-wrap: auto; 8 | } 9 | 10 | .table { 11 | width: 100%; 12 | table-layout: fixed; 13 | } 14 | 15 | .table-wrapper { 16 | /* Navbar Height + Toolbar height */ 17 | max-height: calc(100vh - 90px); 18 | overflow: scroll; 19 | } 20 | 21 | .table-header { 22 | /* position: relative; */ 23 | z-index: 2; 24 | } 25 | 26 | .first-column { 27 | padding-left: var(--rs-space-7); 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/pages/organizations/details/members/members.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | } 4 | 5 | .name-column { 6 | padding-left: var(--rs-space-7); 7 | } 8 | 9 | .empty-state { 10 | height: 100%; 11 | } 12 | 13 | .empty-state-subheading { 14 | max-width: 360px; 15 | text-wrap: auto; 16 | } 17 | 18 | .table { 19 | width: 100%; 20 | table-layout: fixed; 21 | } 22 | 23 | .table-wrapper { 24 | /* Navbar Height + Toolbar height */ 25 | max-height: calc(100vh - 90px); 26 | overflow: scroll; 27 | } 28 | 29 | .table-header { 30 | /* position: relative; */ 31 | z-index: 2; 32 | } 33 | 34 | .table-action-column { 35 | width: 32px; 36 | } 37 | -------------------------------------------------------------------------------- /ui/src/pages/organizations/details/projects/projects.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | } 4 | 5 | .title-column { 6 | padding-left: var(--rs-space-7); 7 | } 8 | 9 | .empty-state { 10 | height: 100%; 11 | } 12 | 13 | .empty-state-subheading { 14 | max-width: 360px; 15 | text-wrap: auto; 16 | } 17 | 18 | .table { 19 | width: 100%; 20 | table-layout: fixed; 21 | } 22 | 23 | .table-wrapper { 24 | /* Navbar Height + Toolbar height */ 25 | max-height: calc(100vh - 90px); 26 | overflow: scroll; 27 | } 28 | 29 | .table-header { 30 | /* position: relative; */ 31 | z-index: 2; 32 | } 33 | 34 | .table-action-column { 35 | width: 32px; 36 | padding: 0; 37 | } 38 | 39 | .flex1 { 40 | flex: 1; 41 | } 42 | -------------------------------------------------------------------------------- /ui/src/pages/organizations/details/security/security.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | } 4 | 5 | .content { 6 | padding-top: var(--rs-space-11); 7 | max-width: 758px; 8 | width: 90%; 9 | } 10 | 11 | .domains-list { 12 | padding: var(--rs-space-1) var(--rs-space-7); 13 | border-radius: var(--rs-radius-2); 14 | border: 1px solid var(--rs-color-border-base-primary); 15 | background: var(--rs-color-background-base-primary); 16 | } 17 | 18 | .domains-list-item { 19 | padding: var(--rs-space-5) 0; 20 | border-bottom: 1px solid var(--rs-color-border-base-primary); 21 | } 22 | 23 | .domains-list-item:last-child { 24 | border-bottom: none; 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/pages/organizations/details/side-panel/side-panel.module.css: -------------------------------------------------------------------------------- 1 | .side-panel-section-item-label { 2 | width: 120px; 3 | } 4 | 5 | .org-details-section-org-id, 6 | .kyc_link { 7 | max-width: 160px; 8 | overflow: hidden; 9 | text-overflow: ellipsis; 10 | white-space: nowrap; 11 | } 12 | 13 | .kyc-status-icon { 14 | height: var(--rs-space-5); 15 | width: var(--rs-space-5); 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/pages/organizations/details/tokens/tokens.module.css: -------------------------------------------------------------------------------- 1 | .empty-state { 2 | height: 100%; 3 | } 4 | 5 | .empty-state-subheading { 6 | max-width: 360px; 7 | text-wrap: auto; 8 | } 9 | 10 | .table { 11 | width: 100%; 12 | table-layout: fixed; 13 | } 14 | 15 | .table-wrapper { 16 | /* Navbar Height + Toolbar height */ 17 | max-height: calc(100vh - 90px); 18 | overflow: scroll; 19 | } 20 | 21 | .table-header { 22 | /* position: relative; */ 23 | z-index: 2; 24 | } 25 | 26 | .first-column { 27 | padding-left: var(--rs-space-7); 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/pages/organizations/details/types.ts: -------------------------------------------------------------------------------- 1 | import { V1Beta1Organization, V1Beta1Role } from "~/api/frontier"; 2 | 3 | export interface OutletContext { 4 | organizationId: string; 5 | organization: V1Beta1Organization; 6 | fetchOrganization: (id: string) => Promise; 7 | roles: V1Beta1Role[]; 8 | } 9 | 10 | export const OrganizationStatus = { 11 | enabled: "enabled", 12 | disabled: "disabled", 13 | }; 14 | 15 | export type OrganizationStatusType = keyof typeof OrganizationStatus; 16 | 17 | export const ORG_NAMESPACE = "app/organization"; 18 | export const PROJECT_NAMESPACE = "app/project"; 19 | 20 | export const DEFAULT_INVITE_ROLE = "app_organization_viewer"; 21 | -------------------------------------------------------------------------------- /ui/src/pages/users/details/audit-log/audit-log.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | max-width: 800px; 4 | margin: 0 auto; 5 | } 6 | 7 | .content { 8 | padding: 24px; 9 | background-color: var(--background-color); 10 | border-radius: 8px; 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/pages/users/details/audit-log/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./audit-log"; 2 | -------------------------------------------------------------------------------- /ui/src/pages/users/details/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user-details"; 2 | -------------------------------------------------------------------------------- /ui/src/pages/users/details/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./layout"; 2 | -------------------------------------------------------------------------------- /ui/src/pages/users/details/layout/layout.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | height: 100vh; 3 | width: 100%; 4 | overflow: hidden; 5 | } 6 | 7 | .main_content { 8 | width: 100%; 9 | transition: width 0.3s ease; 10 | } 11 | 12 | .main_content_with_sidepanel { 13 | width: calc(100% - 400px); 14 | transition: width 0.3s ease; 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/pages/users/details/layout/navbar.module.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | padding: var(--rs-space-4) var(--rs-space-7); 3 | border-bottom: 0.5px solid var(--rs-color-border-base-primary); 4 | background: var(--rs-color-background-base-primary); 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | .nav-chip { 11 | cursor: pointer; 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/pages/users/details/layout/side-panel.module.css: -------------------------------------------------------------------------------- 1 | .side-panel { 2 | transform: all 0.3s ease-in-out; 3 | padding-bottom: 80px; 4 | } 5 | 6 | .side-panel-label { 7 | min-width: 120px; 8 | } 9 | 10 | .text-overflow { 11 | width: fit-content; 12 | max-width: 180px; 13 | overflow: hidden; 14 | text-overflow: ellipsis; 15 | white-space: nowrap; 16 | } 17 | .loader-header { 18 | width: 100%; 19 | height: 32px; 20 | padding: var(--rs-space-3) 0; 21 | } 22 | .loader-header span { 23 | width: 100%; 24 | height: 100%; 25 | } 26 | .dropdown-item { 27 | padding: var(--rs-space-1) 0; 28 | display: flex; 29 | align-items: center; 30 | } 31 | .dropdown-menu-trigger { 32 | width: 100%; 33 | text-align: start; 34 | border-radius: var(--rs-radius-2); 35 | padding: 0 var(--rs-space-3); 36 | height: 32px; 37 | cursor: pointer; 38 | } 39 | .dropdown-menu-trigger:focus { 40 | outline: none; 41 | } 42 | .dropdown-menu-trigger:hover, 43 | .dropdown-menu-trigger:focus-visible { 44 | background-color: var(--rs-color-background-base-primary-hover); 45 | } 46 | -------------------------------------------------------------------------------- /ui/src/pages/users/details/security/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./security"; 2 | -------------------------------------------------------------------------------- /ui/src/pages/users/details/security/security.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | } 4 | 5 | .content { 6 | padding-top: var(--rs-space-11); 7 | max-width: 758px; 8 | width: 90%; 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/pages/users/details/user-context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | import { V1Beta1User } from "~/api/frontier"; 3 | 4 | interface UserContextType { 5 | user: V1Beta1User | undefined; 6 | reset?: () => void; 7 | } 8 | 9 | const UserContext = createContext(undefined); 10 | 11 | export const UserProvider = UserContext.Provider; 12 | 13 | export const useUser = () => { 14 | const context = useContext(UserContext); 15 | if (context === undefined) { 16 | throw new Error("useUser must be used within a UserProvider"); 17 | } 18 | return context; 19 | }; 20 | -------------------------------------------------------------------------------- /ui/src/pages/users/list/index.ts: -------------------------------------------------------------------------------- 1 | export { UsersList } from "./list"; 2 | -------------------------------------------------------------------------------- /ui/src/pages/users/list/invite-users.module.css: -------------------------------------------------------------------------------- 1 | .invite-users-dialog-body { 2 | display: flex; 3 | flex-direction: column; 4 | padding: var(--rs-space-9) var(--rs-space-7); 5 | gap: var(--rs-space-7); 6 | } 7 | 8 | .invite-users-dialog-label { 9 | font-weight: var(--rs-font-weight-medium); 10 | } 11 | 12 | .form-error-message { 13 | color: var(--rs-color-foreground-danger-primary); 14 | } 15 | -------------------------------------------------------------------------------- /ui/src/pages/users/list/list.module.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | padding: var(--rs-space-4) var(--rs-space-7); 3 | border-bottom: 0.5px solid var(--rs-color-border-base-primary); 4 | background: var(--rs-color-background-base-primary); 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | .table { 11 | height: auto; 12 | } 13 | 14 | .table-empty { 15 | height: 100%; 16 | } 17 | 18 | .empty-state { 19 | height: 100%; 20 | } 21 | 22 | .empty-state-subheading { 23 | max-width: 360px; 24 | text-wrap: auto; 25 | } 26 | 27 | .name-column { 28 | padding-left: var(--rs-space-7); 29 | max-width: 200px; 30 | } 31 | 32 | .country-column { 33 | max-width: 150px; 34 | } 35 | 36 | .table-wrapper { 37 | /* Navbar Height + Toolbar height */ 38 | max-height: calc(100vh - 90px); 39 | overflow: scroll; 40 | } 41 | 42 | .table-header { 43 | /* position: relative; */ 44 | z-index: 2; 45 | } 46 | -------------------------------------------------------------------------------- /ui/src/pages/users/util.ts: -------------------------------------------------------------------------------- 1 | import { V1Beta1User } from "~/api/frontier"; 2 | 3 | export const getUserName = (user?: V1Beta1User) => 4 | user?.title || user?.name || ""; 5 | 6 | export const USER_STATES = { 7 | enabled: "Active", 8 | disabled: "Suspended", 9 | } as const; 10 | 11 | export type UserState = keyof typeof USER_STATES; 12 | -------------------------------------------------------------------------------- /ui/src/styles.ts: -------------------------------------------------------------------------------- 1 | export const tableStyle = { 2 | table: { 3 | height: "calc(100vh - 52px)", 4 | }, 5 | tbody: { 6 | tr: { 7 | py: "$3", 8 | }, 9 | }, 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": ".", 19 | "paths": { 20 | "~/*": ["./src/*"], 21 | "~/types/*": ["./types/*"], 22 | "~/stitches": ["./stitches.config.ts"] 23 | } 24 | }, 25 | "include": ["src"], 26 | "references": [{ "path": "./tsconfig.node.json" }] 27 | } 28 | -------------------------------------------------------------------------------- /ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /ui/types/HttpResponse.tsx: -------------------------------------------------------------------------------- 1 | import type { RpcStatus } from "@raystack/frontier"; 2 | 3 | export interface HttpResponse extends Response { 4 | data: unknown; 5 | error: RpcStatus; 6 | } 7 | -------------------------------------------------------------------------------- /ui/types/types.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export type TableColumnMetadata = { 4 | name: ReactNode | Element; 5 | key: string; 6 | value: string; 7 | }; 8 | -------------------------------------------------------------------------------- /ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react-swc"; 2 | import dotenv from "dotenv"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | import svgr from "vite-plugin-svgr"; 6 | dotenv.config(); 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig(() => { 10 | return { 11 | base: "/", 12 | build: { 13 | outDir: "dist/ui", 14 | }, 15 | server: { 16 | proxy: { 17 | "/frontier-api": { 18 | target: process.env.FRONTIER_API_URL, 19 | changeOrigin: true, 20 | rewrite: (path) => path.replace(/^\/frontier-api/, ""), 21 | }, 22 | }, 23 | fs: { 24 | // Allow serving files from one level up to the project root 25 | allow: [".."], 26 | }, 27 | }, 28 | plugins: [react(), svgr(), tsconfigPaths()], 29 | define: { 30 | "process.env": process.env, 31 | }, 32 | }; 33 | }); 34 | --------------------------------------------------------------------------------