├── .dockerignore ├── .env ├── .github └── workflows │ ├── buf.yml │ ├── docker-image-ms.yml │ ├── docker-image.yml │ ├── docker-release.yml │ ├── documentation.yml │ └── publish-dev.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── buf.gen.yaml ├── buf.work.yaml ├── buf └── buf.yaml ├── cmd ├── protoc-gen-go-errors-i18n │ ├── errors.go │ ├── errors │ │ ├── errors.pb.go │ │ └── errors.proto │ ├── errorsTemplate.tpl │ ├── errors_test.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── readme.md │ ├── template.go │ └── version.go └── protoc-gen-go-grpc-proxy │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── proxy.go │ └── readme.md ├── configs.dev ├── config.yaml ├── readme.md └── sys.yaml ├── docker-compose.kafka.yml ├── docker-compose.ms.yml ├── docker-compose.tracing.yml ├── docker-compose.yml ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── blog │ ├── 2023-02-19-背景.mdx │ │ └── index.md │ └── authors.yml ├── docs │ ├── 01-intro │ │ ├── 01-overview.mdx │ │ ├── 02-architecture.mdx │ │ ├── 03-frontend.mdx │ │ ├── 04-develop.mdx │ │ └── _category_.yml │ ├── 02-learn │ │ ├── 01-fundamentals │ │ │ ├── 01-di.mdx │ │ │ ├── 02-gateway.mdx │ │ │ ├── 03-authentication.mdx │ │ │ ├── 04-authorization.mdx │ │ │ ├── 05-vfs.mdx │ │ │ ├── 06-configuration.mdx │ │ │ ├── 07-cache.mdx │ │ │ ├── 08-events.mdx │ │ │ ├── 09-background-job.mdx │ │ │ ├── 10-registry.mdx │ │ │ ├── 15-data-access │ │ │ │ ├── 01-repository.mdx │ │ │ │ ├── 02-orm.mdx │ │ │ │ ├── 03-local-transaction.mdx │ │ │ │ └── _category_.yml │ │ │ ├── 16-distributed-transaction.mdx │ │ │ ├── 17-protobuf.mdx │ │ │ ├── 19-realtime.mdx │ │ │ ├── 20-email-sms.mdx │ │ │ ├── 21-localization.mdx │ │ │ ├── 31-observability │ │ │ │ ├── 01-logging.mdx │ │ │ │ ├── 02-tracing.mdx │ │ │ │ ├── 03-metrics.mdx │ │ │ │ └── _category_.yml │ │ │ └── _category_.yml │ │ ├── 01-web-basic.md │ │ ├── 02-security │ │ │ ├── 01-csrf.mdx │ │ │ ├── 02-cors.mdx │ │ │ ├── 03-logging.mdx │ │ │ ├── 04-ssl.mdx │ │ │ └── _category_.yml │ │ ├── 03-frontend │ │ │ ├── 01-openapi.mdx │ │ │ ├── 02-spa.md │ │ │ ├── 03-react.mdx │ │ │ ├── 04-flutter.mdx │ │ │ └── _category_.yml │ │ └── _category_.yml │ ├── 03-modules │ │ ├── 01-user.mdx │ │ ├── 02-sys.mdx │ │ ├── 03-saas.mdx │ │ ├── 04-realtime.mdx │ │ ├── 05-event.mdx │ │ ├── 06-dtm.mdx │ │ ├── 07-oidc.mdx │ │ ├── 100-gateway.mdx │ │ ├── 200-customize.mdx │ │ ├── 201-create-your-own.mdx │ │ └── _category_.yml │ ├── 04-develop │ │ ├── 01-source codes.mdx │ │ ├── 02-layer.mdx │ │ ├── 03-api.mdx │ │ ├── 04-cache.mdx │ │ ├── 05-event.mdx │ │ ├── 06-i18n.mdx │ │ ├── 07-permission.mdx │ │ ├── 08-menu.mdx │ │ └── _category_.yml │ ├── 05-deploy │ │ ├── 01-local development.mdx │ │ ├── 02-k8s.mdx │ │ └── _category_.yml │ ├── 06-faq │ │ └── _category_.yml │ ├── 07-roadmap │ │ └── _category_.yml │ └── 08-changelog │ │ └── _category_.yml ├── docusaurus.config.js ├── en-US │ └── overview.png ├── i18n │ ├── code.json │ └── zh-Hans │ │ └── docusaurus-plugin-content-docs │ │ ├── current.json │ │ └── current │ │ └── 01-intro │ │ ├── 01-overview.mdx │ │ ├── 02-architecture.mdx │ │ ├── 03-frontend.mdx │ │ └── 04-develop.mdx ├── package.json ├── pnpm-lock.yaml ├── sidebars.js ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── markdown-page.md ├── static │ ├── .nojekyll │ └── img │ │ ├── acl.drawio.png │ │ ├── admin.png │ │ ├── background-job-ui.png │ │ ├── backgroundjob.png │ │ ├── business-module.png │ │ ├── customizable.png │ │ ├── favicon.ico │ │ ├── go-saas-kit.drawio.png │ │ ├── localization.png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── microservice.drawio.png │ │ ├── microservice.png │ │ ├── minimal-module-design.drawio.png │ │ ├── modularity.png │ │ ├── monolithic.drawio.png │ │ ├── multi-tenancy.png │ │ ├── realtime.png │ │ └── role-permission.png ├── tsconfig.json └── zh-CN │ └── overview.png ├── dtm ├── README.md ├── api │ ├── api.go │ └── dtm │ │ └── v1 │ │ ├── dtm.pb.go │ │ ├── dtm.pb.validate.go │ │ ├── dtm.proto │ │ ├── dtm_grpc.pb.go │ │ ├── dtm_grpc_proxy.pb.go │ │ └── dtm_http.pb.go ├── current_version.txt ├── data │ ├── data.go │ ├── migrate.go │ └── sqls │ │ ├── busi.mongo.js │ │ ├── busi.mysql.sql │ │ ├── busi.postgres.sql │ │ ├── dtmcli.barrier.mongo.js │ │ ├── dtmcli.barrier.mysql.sql │ │ ├── dtmcli.barrier.postgres.sql │ │ ├── dtmsvr.storage.mysql.sql │ │ ├── dtmsvr.storage.postgres.sql │ │ └── dtmsvr.storage.tdsql.sql ├── generate.go ├── server │ └── server.go ├── service │ ├── helper.go │ ├── msg.go │ └── service.go └── utils │ └── utils.go ├── event ├── README.md ├── api │ └── v1 │ │ ├── event.go │ │ ├── event.pb.go │ │ ├── event.pb.validate.go │ │ ├── event.proto │ │ ├── event_grpc.pb.go │ │ └── event_grpc_proxy.pb.go ├── consumer.go ├── context.go ├── event.go ├── event.pb.go ├── event.pb.validate.go ├── event.proto ├── factory.go ├── kafka │ ├── kafka.go │ └── trace.go ├── logging.go ├── producer.go ├── proto.go ├── pulsar │ ├── logger.go │ └── pulsar.go ├── recover.go ├── server │ └── server.go ├── service │ ├── event.go │ └── service.go ├── trace │ └── trace.go └── uow.go ├── gateway └── apisix │ ├── Dockerfile │ ├── Makefile │ ├── cmd │ └── go-runner │ │ ├── app.go │ │ ├── main.go │ │ ├── plugins │ │ ├── kit_authn.go │ │ ├── kit_authz.go │ │ ├── resolver.go │ │ └── saas.go │ │ └── version.go │ ├── go.mod │ ├── go.sum │ └── internal │ └── conf │ ├── conf.pb.go │ ├── conf.pb.validate.go │ └── conf.proto ├── go.mod ├── go.sum ├── oidc ├── README.md ├── api │ ├── client │ │ └── v1 │ │ │ ├── client.pb.go │ │ │ ├── client.pb.validate.go │ │ │ ├── client.proto │ │ │ ├── client_grpc.pb.go │ │ │ ├── client_grpc_proxy.pb.go │ │ │ └── client_http.pb.go │ ├── key │ │ └── v1 │ │ │ ├── key.pb.go │ │ │ ├── key.pb.validate.go │ │ │ ├── key.proto │ │ │ ├── key_grpc.pb.go │ │ │ ├── key_grpc_proxy.pb.go │ │ │ └── key_http.pb.go │ ├── permission.go │ └── permission.yaml ├── data │ └── data.go ├── i18n │ ├── embed │ │ ├── en-US.toml │ │ └── zh-CN.toml │ └── i18n.go └── service │ ├── client.go │ ├── key.go │ └── service.go ├── openapi ├── Dockerfile └── kit-merged.swagger.json ├── order ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── api │ ├── api.go │ ├── gateway.go │ ├── gateway.yaml │ ├── order │ │ └── v1 │ │ │ ├── error_reason.pb.go │ │ │ ├── error_reason.pb.validate.go │ │ │ ├── error_reason.proto │ │ │ ├── error_reason_errors.pb.go │ │ │ ├── order.pb.go │ │ │ ├── order.pb.validate.go │ │ │ ├── order.proto │ │ │ ├── order_grpc.pb.go │ │ │ ├── order_grpc_proxy.pb.go │ │ │ ├── order_http.pb.go │ │ │ ├── order_internal.pb.go │ │ │ ├── order_internal.pb.validate.go │ │ │ ├── order_internal.proto │ │ │ ├── order_internal_grpc.pb.go │ │ │ └── order_internal_grpc_proxy.pb.go │ ├── permission.go │ └── permission.yaml ├── buf.gen.yaml ├── cmd │ └── order │ │ └── main.go ├── event │ └── v1 │ │ ├── event.pb.go │ │ ├── event.pb.validate.go │ │ └── event.proto ├── i18n │ ├── embed │ │ ├── en-US.toml │ │ └── zh-CN.toml │ └── i18n.go └── private │ ├── biz │ ├── README.md │ ├── biz.go │ ├── order.go │ └── seed.go │ ├── conf │ ├── conf.pb.go │ ├── conf.pb.validate.go │ └── conf.proto │ ├── data │ ├── README.md │ ├── data.go │ ├── db_test.go │ ├── migrate.go │ └── order.go │ ├── server │ ├── event.go │ ├── grpc.go │ ├── http.go │ └── server.go │ └── service │ ├── README.md │ ├── openapi │ └── api.swagger.json │ ├── order.go │ ├── order_internal.go │ ├── order_notification.go │ └── service.go ├── payment ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── api │ ├── api.go │ ├── checkout │ │ └── v1 │ │ │ ├── checkout.pb.go │ │ │ ├── checkout.pb.validate.go │ │ │ ├── checkout.proto │ │ │ ├── checkout_grpc.pb.go │ │ │ ├── checkout_grpc_proxy.pb.go │ │ │ ├── checkout_http.pb.go │ │ │ ├── error_reason.pb.go │ │ │ ├── error_reason.pb.validate.go │ │ │ ├── error_reason.proto │ │ │ └── error_reason_errors.pb.go │ ├── gateway.go │ ├── gateway.yaml │ ├── gateway │ │ └── v1 │ │ │ ├── gateway.pb.go │ │ │ ├── gateway.pb.validate.go │ │ │ ├── gateway.proto │ │ │ ├── gateway_grpc.pb.go │ │ │ ├── gateway_grpc_proxy.pb.go │ │ │ └── gateway_http.pb.go │ ├── permission.go │ ├── permission.yaml │ └── subscription │ │ └── v1 │ │ ├── error_reason.pb.go │ │ ├── error_reason.pb.validate.go │ │ ├── error_reason.proto │ │ ├── error_reason_errors.pb.go │ │ ├── subscription.pb.go │ │ ├── subscription.pb.validate.go │ │ ├── subscription.proto │ │ ├── subscription_grpc.pb.go │ │ ├── subscription_grpc_proxy.pb.go │ │ ├── subscription_http.pb.go │ │ ├── subscription_internal.pb.go │ │ ├── subscription_internal.pb.validate.go │ │ ├── subscription_internal.proto │ │ ├── subscription_internal_grpc.pb.go │ │ └── subscription_internal_grpc_proxy.pb.go ├── buf.gen.yaml ├── cmd │ └── payment │ │ └── main.go ├── event │ └── v1 │ │ ├── event.pb.go │ │ ├── event.pb.validate.go │ │ └── event.proto ├── i18n │ ├── embed │ │ ├── en-US.toml │ │ └── zh-CN.toml │ └── i18n.go ├── menu │ ├── menu.go │ └── menu.yaml └── private │ ├── biz │ ├── README.md │ ├── biz.go │ ├── seed.go │ └── subscription.go │ ├── conf │ ├── conf.go │ ├── conf.pb.go │ ├── conf.pb.validate.go │ └── conf.proto │ ├── data │ ├── README.md │ ├── data.go │ ├── migrate.go │ └── subscription.go │ ├── server │ ├── grpc.go │ ├── http.go │ └── server.go │ └── service │ ├── README.md │ ├── checkout.go │ ├── checkout_stripe.go │ ├── openapi │ └── api.swagger.json │ ├── payment.go │ ├── service.go │ ├── subscription.go │ ├── subscription_internal.go │ └── subscription_stripe.go ├── pkg ├── api │ ├── api.go │ ├── middleware.go │ ├── propagator.go │ ├── token_mgr.go │ └── trust.go ├── apisix │ ├── admin.go │ ├── apisix.go │ ├── apisix.pb.go │ ├── apisix.pb.validate.go │ ├── apisix.proto │ └── watch_sync.go ├── authn │ ├── context.go │ ├── helper.go │ ├── jwt │ │ ├── authentication.go │ │ ├── context.go │ │ ├── extract.go │ │ ├── jwt.go │ │ └── tokenizer.go │ ├── session │ │ ├── context.go │ │ ├── session.go │ │ └── state.go │ └── user_info.go ├── authz │ ├── authz │ │ ├── action.go │ │ ├── checker.go │ │ ├── context.go │ │ ├── def.go │ │ ├── def.pb.go │ │ ├── def.pb.validate.go │ │ ├── def.proto │ │ ├── permission.go │ │ ├── resource.go │ │ ├── result.go │ │ ├── service.go │ │ ├── subject_resolver.go │ │ ├── subjects.go │ │ └── types.go │ ├── casbin │ │ ├── casbin.go │ │ ├── checker.go │ │ ├── model.conf │ │ └── permission.go │ └── readme.md ├── blob │ ├── blob.go │ ├── blob.pb.go │ ├── blob.pb.validate.go │ ├── blob.proto │ ├── memory │ │ └── memory.go │ ├── os │ │ └── os.go │ └── s3 │ │ └── s3.go ├── cache │ ├── helper.go │ └── proto.go ├── conf │ ├── conf.go │ ├── conf.pb.go │ ├── conf.pb.validate.go │ ├── conf.proto │ └── vfs.go ├── csrf │ └── csrf.go ├── dal │ ├── dal.go │ └── readme.md ├── data │ ├── data.pb.go │ ├── data.pb.validate.go │ ├── data.proto │ ├── json_map.go │ ├── readme.md │ ├── repo.go │ └── value.go ├── di │ └── di.go ├── email │ ├── conf.go │ ├── email.go │ ├── email.pb.go │ ├── email.pb.validate.go │ ├── email.proto │ ├── factory.go │ ├── log │ │ └── log.go │ └── smtp │ │ └── smtp.go ├── errors │ ├── errors.go │ └── stack.go ├── flag │ └── flag.go ├── gorm │ ├── agg.go │ ├── audit.go │ ├── audit_test.go │ ├── base.go │ ├── context.go │ ├── gorm.go │ ├── logger.go │ ├── query.go │ ├── repo.go │ └── scopes.go ├── idgen │ ├── idgen.go │ ├── ksuid.go │ ├── shortuuid.go │ └── uuid.go ├── idp │ ├── idp.go │ ├── idp.pb.go │ ├── idp.pb.validate.go │ ├── idp.proto │ ├── idp_errors.pb.go │ ├── idp_errors.pb.validate.go │ ├── idp_errors.proto │ ├── idp_errors_errors.pb.go │ ├── stripe.go │ └── wechat.go ├── job │ ├── job.go │ ├── server.go │ ├── tracing.go │ └── ui.go ├── localize │ └── localize.go ├── logging │ ├── factory.go │ └── logging.go ├── mapstructure │ └── mapstructure.go ├── multilingual │ ├── gorm │ │ ├── gorm.go │ │ └── gorm_test.go │ ├── multilingual.go │ └── multilingual_test.go ├── price │ ├── price.go │ ├── price.pb.go │ ├── price.pb.validate.go │ ├── price.proto │ └── price_test.go ├── query │ ├── operation.go │ ├── operation.pb.go │ ├── operation.pb.validate.go │ ├── operation.proto │ └── query.go ├── redis │ ├── redis.go │ ├── redis.pb.go │ ├── redis.pb.validate.go │ └── redis.proto ├── registry │ ├── etcd │ │ ├── README.md │ │ ├── etcd.go │ │ ├── registry.go │ │ ├── service.go │ │ └── watcher.go │ ├── registry.go │ ├── registry.pb.go │ ├── registry.pb.validate.go │ └── registry.proto ├── server │ ├── common │ │ └── common.go │ ├── endpoint │ │ ├── endpoint.go │ │ ├── endpoint_test.go │ │ └── readme.md │ ├── errors.go │ ├── grpc │ │ └── grpc.go │ ├── http │ │ ├── convert.go │ │ └── http.go │ ├── saas.go │ ├── seed.go │ └── server.go ├── sms │ └── sms.go ├── sortable │ ├── sortable.go │ └── sortable_test.go ├── stripe │ ├── stripe.go │ ├── stripe.pb.go │ ├── stripe.pb.validate.go │ └── stripe.proto ├── tracers │ └── tracers.go ├── uow │ ├── event │ │ └── event.go │ └── uow.go └── utils │ ├── hash.go │ ├── pb.go │ ├── sort.go │ └── utils.go ├── product ├── Dockerfile ├── Makefile ├── README.md ├── api │ ├── api.go │ ├── category │ │ └── v1 │ │ │ ├── category.pb.go │ │ │ ├── category.pb.validate.go │ │ │ ├── category.proto │ │ │ ├── category_grpc.pb.go │ │ │ ├── category_grpc_proxy.pb.go │ │ │ └── category_http.pb.go │ ├── gateway.go │ ├── gateway.yaml │ ├── permission.go │ ├── permission.yaml │ ├── price │ │ └── v1 │ │ │ ├── price.pb.go │ │ │ ├── price.pb.validate.go │ │ │ └── price.proto │ └── product │ │ └── v1 │ │ ├── brand.pb.go │ │ ├── brand.pb.validate.go │ │ ├── brand.proto │ │ ├── brand_grpc.pb.go │ │ ├── brand_grpc_proxy.pb.go │ │ ├── brand_http.pb.go │ │ ├── common.pb.go │ │ ├── common.pb.validate.go │ │ ├── common.proto │ │ ├── error_reason.pb.go │ │ ├── error_reason.pb.validate.go │ │ ├── error_reason.proto │ │ ├── error_reason_errors.pb.go │ │ ├── product.pb.go │ │ ├── product.pb.validate.go │ │ ├── product.proto │ │ ├── product_grpc.pb.go │ │ ├── product_grpc_proxy.pb.go │ │ ├── product_http.pb.go │ │ ├── product_internal.pb.go │ │ ├── product_internal.pb.validate.go │ │ ├── product_internal.proto │ │ ├── product_internal_grpc.pb.go │ │ ├── product_internal_grpc_proxy.pb.go │ │ ├── product_sku.pb.go │ │ ├── product_sku.pb.validate.go │ │ └── product_sku.proto ├── buf.gen.yaml ├── cmd │ └── product │ │ └── main.go ├── event │ └── v1 │ │ ├── event.pb.go │ │ ├── event.pb.validate.go │ │ └── event.proto ├── i18n │ ├── embed │ │ ├── en-US.toml │ │ └── zh-CN.toml │ └── i18n.go ├── menu │ ├── menu.go │ └── menu.yaml └── private │ ├── biz │ ├── README.md │ ├── biz.go │ ├── brand.go │ ├── category.go │ ├── job.pb.go │ ├── job.pb.validate.go │ ├── job.proto │ ├── media.go │ ├── price.go │ ├── product.go │ ├── product_sku.go │ ├── seed.go │ └── sync_link.go │ ├── conf │ ├── conf.pb.go │ ├── conf.pb.validate.go │ └── conf.proto │ ├── data │ ├── README.md │ ├── category.go │ ├── data.go │ ├── db_test.go │ ├── media.go │ ├── migrate.go │ ├── price.go │ └── product.go │ ├── server │ ├── grpc.go │ ├── http.go │ ├── job.go │ └── server.go │ └── service │ ├── openapi │ └── api.swagger.json │ ├── price.go │ ├── product.go │ ├── product_internal.go │ ├── product_updated_job.go │ ├── service.go │ └── upload.go ├── proto ├── errors │ └── errors.proto ├── google │ ├── api │ │ ├── annotations.proto │ │ ├── client.proto │ │ ├── field_behavior.proto │ │ ├── http.proto │ │ └── httpbody.proto │ ├── protobuf │ │ └── descriptor.proto │ └── rpc │ │ ├── error_details.proto │ │ └── status.proto ├── lbs │ └── address.proto ├── protoc-gen-openapiv2 │ └── options │ │ ├── annotations.proto │ │ └── openapiv2.proto ├── readme.md └── validate │ ├── README.md │ └── validate.proto ├── quickstart ├── configs │ ├── apisix │ │ ├── apisix_conf │ │ │ └── config.yaml │ │ └── dashboard_conf │ │ │ └── conf.yaml │ ├── config.yaml │ ├── dtm │ │ └── conf.yml │ ├── hydra │ │ └── hydra.yml │ ├── otel │ │ └── otel-collector-config.yaml │ ├── payment.yaml │ ├── saas.yaml │ ├── sys.yaml │ └── user.yaml └── demo │ ├── dtm │ └── conf.yml │ ├── hydra │ └── hydra.yml │ ├── z.config.yaml │ └── z.sys.yaml ├── realtime ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── api │ ├── api.go │ ├── gateway.go │ ├── gateway.yaml │ └── notification │ │ └── v1 │ │ ├── notification.pb.go │ │ ├── notification.pb.validate.go │ │ ├── notification.proto │ │ ├── notification_grpc.pb.go │ │ ├── notification_grpc_proxy.pb.go │ │ └── notification_http.pb.go ├── buf.gen.yaml ├── cmd │ └── realtime │ │ └── main.go ├── event │ └── v1 │ │ ├── event.pb.go │ │ ├── event.pb.validate.go │ │ └── event.proto ├── i18n │ ├── embed │ │ ├── en-US.toml │ │ └── zh-CN.toml │ └── i18n.go └── private │ ├── biz │ ├── README.md │ ├── biz.go │ └── notification.go │ ├── conf │ ├── conf.pb.go │ ├── conf.pb.validate.go │ └── conf.proto │ ├── data │ ├── README.md │ ├── data.go │ ├── migrate.go │ └── notification.go │ ├── server │ ├── centrifuge.go │ ├── event.go │ ├── grpc.go │ ├── http.go │ └── server.go │ └── service │ ├── README.md │ ├── centrifuge.go │ ├── event.go │ ├── notification.go │ ├── openapi │ └── api.swagger.json │ └── service.go ├── saas ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── api │ ├── api.go │ ├── gateway.go │ ├── gateway.yaml │ ├── permission.go │ ├── permission.yaml │ ├── plan │ │ └── v1 │ │ │ ├── error_reason.pb.go │ │ │ ├── error_reason.pb.validate.go │ │ │ ├── error_reason.proto │ │ │ ├── error_reason_errors.pb.go │ │ │ ├── plan.pb.go │ │ │ ├── plan.pb.validate.go │ │ │ ├── plan.proto │ │ │ ├── plan_grpc.pb.go │ │ │ ├── plan_grpc_proxy.pb.go │ │ │ └── plan_http.pb.go │ ├── tenant │ │ └── v1 │ │ │ ├── error_reason.pb.go │ │ │ ├── error_reason.pb.validate.go │ │ │ ├── error_reason.proto │ │ │ ├── error_reason_errors.pb.go │ │ │ ├── tenant.go │ │ │ ├── tenant.pb.go │ │ │ ├── tenant.pb.validate.go │ │ │ ├── tenant.proto │ │ │ ├── tenant_grpc.pb.go │ │ │ ├── tenant_grpc_proxy.pb.go │ │ │ └── tenant_http.pb.go │ └── tenant_store.go ├── buf.gen.yaml ├── cmd │ └── saas │ │ └── main.go ├── event │ └── v1 │ │ ├── event.pb.go │ │ ├── event.pb.validate.go │ │ └── event.proto ├── i18n │ ├── embed │ │ ├── en-US.toml │ │ └── zh-CN.toml │ └── i18n.go └── private │ ├── biz │ ├── README.md │ ├── biz.go │ ├── conn_str_generator.go │ ├── plan.go │ ├── tenant.go │ └── tenant_event_handler.go │ ├── conf │ ├── conf.pb.go │ ├── conf.pb.validate.go │ └── conf.proto │ ├── data │ ├── README.md │ ├── data.go │ ├── migrate.go │ ├── plan.go │ └── tenant.go │ ├── server │ ├── event.go │ ├── grpc.go │ ├── http.go │ ├── job.go │ └── server.go │ └── service │ ├── README.md │ ├── openapi │ └── api.swagger.json │ ├── plan.go │ ├── service.go │ ├── tenant.go │ └── tenant_internal.go ├── sys ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── api │ ├── api.go │ ├── gateway.go │ ├── gateway.yaml │ ├── locale │ │ └── v1 │ │ │ ├── locale.pb.go │ │ │ ├── locale.pb.validate.go │ │ │ ├── locale.proto │ │ │ ├── locale_grpc.pb.go │ │ │ ├── locale_grpc_proxy.pb.go │ │ │ └── locale_http.pb.go │ ├── menu │ │ └── v1 │ │ │ ├── error_reason.pb.go │ │ │ ├── error_reason.pb.validate.go │ │ │ ├── error_reason.proto │ │ │ ├── error_reason_errors.pb.go │ │ │ ├── menu.pb.go │ │ │ ├── menu.pb.validate.go │ │ │ ├── menu.proto │ │ │ ├── menu_grpc.pb.go │ │ │ ├── menu_grpc_proxy.pb.go │ │ │ └── menu_http.pb.go │ ├── permission.go │ └── permission.yaml ├── buf.gen.yaml ├── cmd │ └── sys │ │ └── main.go ├── i18n │ ├── embed │ │ ├── en-US.toml │ │ └── zh-CN.toml │ └── i18n.go ├── menu │ ├── menu.go │ └── menu.yaml └── private │ ├── biz │ ├── README.md │ ├── apisix_migration_task.go │ ├── apisix_seed.go │ ├── biz.go │ ├── menu.go │ └── menu_seed.go │ ├── conf │ ├── conf.pb.go │ ├── conf.pb.validate.go │ └── conf.proto │ ├── data │ ├── README.md │ ├── data.go │ ├── menu.go │ └── migrate.go │ ├── server │ ├── grpc.go │ ├── http.go │ ├── job.go │ └── server.go │ └── service │ ├── README.md │ ├── locale.go │ ├── menu.go │ ├── openapi │ └── api.swagger.json │ └── service.go └── user ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── api ├── account │ └── v1 │ │ ├── account.pb.go │ │ ├── account.pb.validate.go │ │ ├── account.proto │ │ ├── account_grpc.pb.go │ │ ├── account_grpc_proxy.pb.go │ │ └── account_http.pb.go ├── api.go ├── auth │ └── v1 │ │ ├── auth.go │ │ ├── auth.pb.go │ │ ├── auth.pb.validate.go │ │ ├── auth.proto │ │ ├── auth_grpc.pb.go │ │ ├── auth_grpc_proxy.pb.go │ │ ├── auth_http.pb.go │ │ ├── error_reason.pb.go │ │ ├── error_reason.pb.validate.go │ │ ├── error_reason.proto │ │ ├── error_reason_errors.pb.go │ │ ├── idp.pb.go │ │ ├── idp.pb.validate.go │ │ ├── idp.proto │ │ ├── idp_grpc.pb.go │ │ ├── idp_grpc_proxy.pb.go │ │ ├── idp_http.pb.go │ │ ├── web.pb.go │ │ ├── web.pb.validate.go │ │ ├── web.proto │ │ ├── web_grpc.pb.go │ │ ├── web_grpc_proxy.pb.go │ │ └── web_http.pb.go ├── gateway.go ├── gateway.yaml ├── permission.go ├── permission.yaml ├── permission │ └── v1 │ │ ├── permission.pb.go │ │ ├── permission.pb.validate.go │ │ ├── permission.proto │ │ ├── permission_grpc.pb.go │ │ ├── permission_grpc_proxy.pb.go │ │ ├── permission_http.pb.go │ │ ├── permission_internal.pb.go │ │ ├── permission_internal.pb.validate.go │ │ ├── permission_internal.proto │ │ ├── permission_internal_grpc.pb.go │ │ └── permission_internal_grpc_proxy.pb.go ├── remote_permission_checker.go ├── role │ └── v1 │ │ ├── error_reason.pb.go │ │ ├── error_reason.pb.validate.go │ │ ├── error_reason.proto │ │ ├── error_reason_errors.pb.go │ │ ├── role.pb.go │ │ ├── role.pb.validate.go │ │ ├── role.proto │ │ ├── role_grpc.pb.go │ │ ├── role_grpc_proxy.pb.go │ │ └── role_http.pb.go ├── session.go ├── user │ └── v1 │ │ ├── error_reason.pb.go │ │ ├── error_reason.pb.validate.go │ │ ├── error_reason.proto │ │ ├── error_reason_errors.pb.go │ │ ├── user.go │ │ ├── user.pb.go │ │ ├── user.pb.validate.go │ │ ├── user.proto │ │ ├── user_admin.pb.go │ │ ├── user_admin.pb.validate.go │ │ ├── user_admin.proto │ │ ├── user_admin_grpc.pb.go │ │ ├── user_admin_grpc_proxy.pb.go │ │ ├── user_admin_http.pb.go │ │ ├── user_grpc.pb.go │ │ ├── user_grpc_proxy.pb.go │ │ └── user_http.pb.go └── user_tenant_contrib.go ├── buf.gen.yaml ├── cmd └── user │ └── main.go ├── event └── v1 │ ├── event.pb.go │ ├── event.pb.validate.go │ └── event.proto ├── i18n ├── embed │ ├── en-US.toml │ └── zh-CN.toml └── i18n.go ├── private ├── biz │ ├── README.md │ ├── biz.go │ ├── cache.pb.go │ ├── cache.pb.validate.go │ ├── cache.proto │ ├── email_sender.go │ ├── lookup_normalizer.go │ ├── otp.go │ ├── password_hasher.go │ ├── password_hasher_test.go │ ├── password_validator.go │ ├── permission_seeder.go │ ├── refresh_token.go │ ├── role.go │ ├── role_seed.go │ ├── signin.go │ ├── user.go │ ├── user_addr.go │ ├── user_login.go │ ├── user_manager.go │ ├── user_role.go │ ├── user_seed.go │ ├── user_setting.go │ ├── user_tenant.go │ ├── user_token.go │ ├── user_token_provider.go │ └── user_validator.go ├── conf │ ├── conf.pb.go │ ├── conf.pb.validate.go │ └── conf.proto ├── data │ ├── README.md │ ├── data.go │ ├── db_test.go │ ├── migrate.go │ ├── refresh_token.go │ ├── repo.go │ ├── role.go │ ├── user.go │ ├── user_addr.go │ ├── user_setting.go │ ├── user_tenant.go │ └── user_token.go ├── server │ ├── event.go │ ├── grpc.go │ ├── http.go │ ├── job.go │ └── server.go └── service │ ├── README.md │ ├── account.go │ ├── auth.go │ ├── http │ └── auth.go │ ├── openapi │ └── api.swagger.json │ ├── permission.go │ ├── permission_internal.go │ ├── role.go │ ├── service.go │ ├── user.go │ ├── user_admin.go │ ├── user_internal.go │ ├── user_role_contrib.go │ └── wechatauth.go └── util └── permission.go /.dockerignore: -------------------------------------------------------------------------------- 1 | docker-compose.yml 2 | docker-compose.*.yml 3 | 4 | # Develop tools 5 | .vscode/ 6 | .idea/ 7 | .run/ 8 | *.swp 9 | 10 | **/*/.temp/ 11 | **/*/node_modules -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DOCKER_REGISTRY=goxiaoy/ 2 | DOCKER_TAG=dev -------------------------------------------------------------------------------- /.github/workflows/buf.yml: -------------------------------------------------------------------------------- 1 | name: Push Buf Proto 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: 8 | - 'main' 9 | workflow_dispatch: 10 | 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: bufbuild/buf-setup-action@v1.5.0 19 | - name: link 20 | run: make link 21 | 22 | - name: test buf build 23 | run: buf build 24 | 25 | - uses: bufbuild/buf-push-action@v1.0.1 26 | if: | 27 | github.event_name == 'push' 28 | && github.repository == 'go-saas/kit' 29 | with: 30 | input: buf 31 | buf_token: ${{ secrets.BUF_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: 8 | - 'main' 9 | workflow_dispatch: 10 | 11 | env: 12 | DOCKER_TAG: dev 13 | 14 | jobs: 15 | 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Build the docker image 22 | run: docker-compose -f docker-compose.yml build 23 | 24 | - name: Docker login 25 | if: | 26 | github.event_name == 'push' 27 | && github.repository == 'go-saas/kit' 28 | uses: docker/login-action@v1 29 | with: 30 | username: ${{ secrets.DOCKER_USERNAME }} 31 | password: ${{ secrets.DOCKER_TOKEN }} 32 | 33 | - name: Push docker image 34 | if: | 35 | github.event_name == 'push' 36 | && github.repository == 'go-saas/kit' 37 | 38 | run: docker-compose -f docker-compose.yml push 39 | 40 | 41 | -------------------------------------------------------------------------------- /.github/workflows/docker-release.yml: -------------------------------------------------------------------------------- 1 | name: Docker Release Image 2 | 3 | on: 4 | push: 5 | # Pattern matched against refs/tags 6 | tags: 7 | - '**' # Push events to every tag including hierarchical tags like v1.0/beta 8 | 9 | env: 10 | DOCKER_TAG: ${{github.ref_name}} 11 | 12 | jobs: 13 | 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build the docker image 20 | run: docker-compose -f docker-compose.yml -f docker-compose.ms.yml build 21 | 22 | - name: Docker login 23 | if: | 24 | github.event_name == 'push' 25 | && github.repository == 'go-saas/kit' 26 | uses: docker/login-action@v1 27 | with: 28 | username: ${{ secrets.DOCKER_USERNAME }} 29 | password: ${{ secrets.DOCKER_TOKEN }} 30 | 31 | - name: Push docker image 32 | if: | 33 | github.event_name == 'push' 34 | && github.repository == 'go-saas/kit' 35 | 36 | run: docker-compose -f docker-compose.yml -f docker-compose.ms.yml push 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Reference https://github.com/github/gitignore/blob/master/Go.gitignore 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 19 | *.o 20 | *.a 21 | *.so 22 | 23 | # OS General 24 | Thumbs.db 25 | .DS_Store 26 | 27 | # project 28 | *.cert 29 | *.key 30 | *.log 31 | bin/ 32 | 33 | # Develop tools 34 | .idea/ 35 | .run/ 36 | *.swp 37 | 38 | 39 | .temp/ 40 | 41 | .assets/ 42 | 43 | buf/* 44 | !buf/buf.yaml 45 | 46 | # Local config files 47 | *.local.yml 48 | *.local.yaml -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - name: go 4 | out: ./buf 5 | opt: paths=source_relative 6 | - name: go-http 7 | out: ./buf 8 | opt: paths=source_relative 9 | - name: go-grpc 10 | out: ./buf 11 | opt: 12 | - paths=source_relative 13 | - require_unimplemented_servers=false 14 | - name: go-grpc-proxy 15 | out: ./buf 16 | opt: 17 | - paths=source_relative 18 | - name: validate 19 | out: ./buf 20 | opt: 21 | - paths=source_relative 22 | - lang=go 23 | - name: go-errors-i18n 24 | out: ./buf 25 | opt: 26 | - paths=source_relative 27 | - plugin: buf.build/grpc-ecosystem/openapiv2:v2.15.2 28 | out: ./openapi 29 | opt: 30 | - allow_merge=true 31 | - proto3_optional_nullable=true 32 | - merge_file_name=kit-merged 33 | -------------------------------------------------------------------------------- /buf.work.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | directories: 3 | - buf -------------------------------------------------------------------------------- /buf/buf.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | name: buf.build/goxy/go-saas-kit -------------------------------------------------------------------------------- /cmd/protoc-gen-go-errors-i18n/errors/errors.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package errors; 4 | 5 | option go_package = "github.com/go-kratos/kratos/v2/errors;errors"; 6 | option java_multiple_files = true; 7 | option java_package = "com.github.kratos.errors"; 8 | option objc_class_prefix = "KratosErrors"; 9 | 10 | import "google/protobuf/descriptor.proto"; 11 | 12 | message Error { 13 | int32 code = 1; 14 | string reason = 2; 15 | string message = 3; 16 | map metadata = 4; 17 | }; 18 | 19 | extend google.protobuf.EnumOptions { 20 | int32 default_code = 1108; 21 | } 22 | 23 | extend google.protobuf.EnumValueOptions { 24 | int32 code = 1109; 25 | } 26 | -------------------------------------------------------------------------------- /cmd/protoc-gen-go-errors-i18n/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-saas/kit/cmd/protoc-gen-go-errors-i18n/v2 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.7 // indirect 7 | golang.org/x/text v0.3.7 8 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 9 | google.golang.org/protobuf v1.28.0 10 | ) 11 | -------------------------------------------------------------------------------- /cmd/protoc-gen-go-errors-i18n/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "google.golang.org/protobuf/compiler/protogen" 8 | "google.golang.org/protobuf/types/pluginpb" 9 | ) 10 | 11 | var showVersion = flag.Bool("version", false, "print the version and exit") 12 | 13 | func main() { 14 | flag.Parse() 15 | if *showVersion { 16 | fmt.Printf("protoc-gen-go-errors %v\n", release) 17 | return 18 | } 19 | var flags flag.FlagSet 20 | protogen.Options{ 21 | ParamFunc: flags.Set, 22 | }.Run(func(gen *protogen.Plugin) error { 23 | gen.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) 24 | for _, f := range gen.Files { 25 | if !f.Generate { 26 | continue 27 | } 28 | generateFile(gen, f) 29 | } 30 | return nil 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /cmd/protoc-gen-go-errors-i18n/readme.md: -------------------------------------------------------------------------------- 1 | # protoc-gen-go-errors-i18n 2 | 3 | generate [i18n](https://github.com/go-saas/go-i18n) method for [errors](https://github.com/go-kratos/kratos/tree/main/cmd/protoc-gen-go-errors) 4 | 5 | for example 6 | ```go 7 | func ErrorInvalidCredentialsLocalized(localizer *i18n.Localizer, data map[string]interface{}, pluralCount interface{}) *errors.Error { 8 | msg, err := localizer.Localize(&i18n.LocalizeConfig{ 9 | DefaultMessage: &i18n.Message{ 10 | ID: "InvalidCredentials", 11 | }, 12 | TemplateData: data, 13 | PluralCount: pluralCount, 14 | }) 15 | if err == nil { 16 | return errors.New(400, ErrorReason_INVALID_CREDENTIALS.String(), msg) 17 | } else { 18 | return errors.New(400, ErrorReason_INVALID_CREDENTIALS.String(), "") 19 | } 20 | 21 | } 22 | ``` -------------------------------------------------------------------------------- /cmd/protoc-gen-go-errors-i18n/template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "text/template" 7 | ) 8 | 9 | //go:embed errorsTemplate.tpl 10 | var errorsTemplate string 11 | 12 | type errorInfo struct { 13 | Name string 14 | Value string 15 | HTTPCode int 16 | CamelValue string 17 | Comment string 18 | HasComment bool 19 | MsgKey string 20 | } 21 | 22 | type errorWrapper struct { 23 | Errors []*errorInfo 24 | } 25 | 26 | func (e *errorWrapper) execute() string { 27 | buf := new(bytes.Buffer) 28 | tmpl, err := template.New("errors").Parse(errorsTemplate) 29 | if err != nil { 30 | panic(err) 31 | } 32 | if err := tmpl.Execute(buf, e); err != nil { 33 | panic(err) 34 | } 35 | return buf.String() 36 | } 37 | -------------------------------------------------------------------------------- /cmd/protoc-gen-go-errors-i18n/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // release is the current protoc-gen-go-errors version. 4 | const release = "v2.6.2" 5 | -------------------------------------------------------------------------------- /cmd/protoc-gen-go-grpc-proxy/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-saas/kit/cmd/protoc-gen-go-grpc-proxy 2 | 3 | go 1.16 4 | 5 | require google.golang.org/protobuf v1.28.0 6 | -------------------------------------------------------------------------------- /cmd/protoc-gen-go-grpc-proxy/go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 2 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 3 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 4 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 5 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 6 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 7 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 8 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 9 | -------------------------------------------------------------------------------- /cmd/protoc-gen-go-grpc-proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "google.golang.org/protobuf/compiler/protogen" 8 | "google.golang.org/protobuf/types/pluginpb" 9 | ) 10 | 11 | const version = "1.2.0" 12 | 13 | var requireUnimplemented *bool 14 | 15 | func main() { 16 | showVersion := flag.Bool("version", false, "print the version and exit") 17 | flag.Parse() 18 | if *showVersion { 19 | fmt.Printf("protoc-gen-go-grpc %v\n", version) 20 | return 21 | } 22 | 23 | var flags flag.FlagSet 24 | requireUnimplemented = flags.Bool("require_unimplemented_servers", true, "set to false to match legacy behavior") 25 | 26 | protogen.Options{ 27 | ParamFunc: flags.Set, 28 | }.Run(func(gen *protogen.Plugin) error { 29 | gen.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) 30 | for _, f := range gen.Files { 31 | if !f.Generate { 32 | continue 33 | } 34 | generateFile(gen, f) 35 | } 36 | return nil 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/protoc-gen-go-grpc-proxy/readme.md: -------------------------------------------------------------------------------- 1 | # protoc-gen-go-grpc-proxy 2 | 3 | generate proxy method for [protoc-gen-go-grpc](https://github.com/grpc/grpc-go/tree/master/cmd/protoc-gen-go-grpc) to unify server and client interface -------------------------------------------------------------------------------- /configs.dev/readme.md: -------------------------------------------------------------------------------- 1 | # This is the config override folder for development only 2 | 3 | You can add the following program arguments 4 | ```shell 5 | -conf ./quickstart/configs -conf ./configs.dev 6 | ``` -------------------------------------------------------------------------------- /configs.dev/sys.yaml: -------------------------------------------------------------------------------- 1 | sys: 2 | apisix: 3 | endpoint: "http://host.docker.internal:9280" -------------------------------------------------------------------------------- /docker-compose.tracing.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | # Jaeger 4 | jaeger-all-in-one: 5 | image: jaegertracing/all-in-one:latest 6 | # ports: 7 | # - "16686:16686" # jaeger ui port 8 | # - "14268" 9 | environment: 10 | - QUERY_BASE_PATH=/jaeger 11 | restart: unless-stopped 12 | otel-collector: 13 | image: otel/opentelemetry-collector-contrib:0.89.0 14 | command: ["--config=/etc/otel-collector-config.yaml"] 15 | volumes: 16 | - ./quickstart/configs/otel/otel-collector-config.yaml:/etc/otel-collector-config.yaml 17 | restart: unless-stopped 18 | ports: 19 | # - "1888" # pprof extension 20 | # - "8888:8888" # Prometheus metrics exposed by the collector 21 | # - "8889:8889" # Prometheus exporter metrics 22 | # - "13133:13133" # health_check extension 23 | - "4317:4317" # OTLP gRPC receiver 24 | - "4318:4318" # Add OTLP HTTP Receiver port mapping, required for Apisix 25 | # - "55670:55679" # zpages extension 26 | 27 | depends_on: 28 | - jaeger-all-in-one 29 | -------------------------------------------------------------------------------- /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* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/blog/2023-02-19-背景.mdx/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: 背景 3 | title: 背景 4 | authors: [goxy] 5 | tags: [go,saas] 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/blog/authors.yml: -------------------------------------------------------------------------------- 1 | goxy: 2 | name: Goxy 3 | title: Author of GO-SAAS-KIT 4 | url: https://github.com/goxiaoy 5 | image_url: https://github.com/goxiaoy.png 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/docs/01-intro/04-develop.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Customize & Develop 3 | title: Customize & Develop 4 | --- 5 | 6 | ## Customize 7 | 8 | You can checkout [commerce](https://github.com/go-saas/commerce) to see how to customize existing modules 9 | https://github.com/go-saas/commerce/tree/main/user 10 | 11 | For more information, go to [customize existing modules](../modules/customize) 12 | 13 | ## Develop your own services 14 | 15 | The creation of new services is powered by [kratos layout](https://go-kratos.dev/en/docs/getting-started/start/#project-layout) 16 | 17 | ```shell 18 | kratos new -r https://github.com/go-saas/kit-layout.git 19 | ``` 20 | 21 | For more information, go to [create your own module](../modules/create-your-own) -------------------------------------------------------------------------------- /docs/docs/01-intro/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Introduction" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/07-cache.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Cache 3 | title: Cache 4 | --- 5 | 6 | In a web system, a cache refers to a mechanism that stores and manages copies of frequently accessed data, resources, or web content in a temporary storage location. The primary purpose of a cache is to improve the performance and responsiveness of a web application or website by reducing the need to fetch data or resources from their original, slower sources. Caching is a fundamental technique used to speed up web systems and optimize resource utilization. 7 | 8 | Cache is based on [gocache](https://github.com/eko/gocache) and data format is defined as [Protobuf](./protobuf) 9 | 10 | 11 | ## [Redis](https://redis.com/) 12 | 13 | ## Cache Consistency 14 | 15 | - Event-Driven Invalidation: 16 | Cache invalidation events are generated whenever relevant data changes(after [local transactions](./data-access/local-transaction)). Subscribers listen to these events and invalidate or update their caches accordingly. This approach ensures that caches stay in sync with data changes. -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/15-data-access/02-orm.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Orm 3 | title: Orm 4 | --- 5 | 6 | ORM stands for Object-Relational Mapping. It is a programming technique and a software design pattern used in software development to bridge the gap between object-oriented programming languages and relational database systems. The primary goal of ORM is to simplify and streamline the process of working with databases in object-oriented code. 7 | 8 | ORM allows us to implement row-level SaaS data isolation more quickly and securely, and the underlying [go-saas/saas](https://github.com/go-saas/saas) project supports both [ent](https://entgo.io/) and [gorm](https://gorm.io/index.html). 9 | 10 | ## [Gorm](https://gorm.io/index.html) -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/15-data-access/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Data Access" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/16-distributed-transaction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Distributed Transaction 3 | title: Distributed Transaction 4 | --- 5 | 6 | Kit integrate with [Dtm](https://en.dtm.pub/) to accomplish Distributed Transaction 7 | 8 | 9 | see https://github.com/go-saas/kit/blob/main/dtm/service/helper.go 10 | 11 | ### Two-phase message 12 | 13 | See https://en.dtm.pub/guide/e-msg.html#how-to-guarantee-atomicity 14 | 15 | 16 | 17 | ## Refer 18 | https://github.com/go-saas/kit/issues/19 -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/17-protobuf.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Protobuf 3 | title: Protobuf 4 | --- 5 | Protocol Buffers, often abbreviated as Protobuf, is a language-agnostic, binary serialization format developed by Google. It is used for efficiently and reliably serializing structured data, making it easier to exchange data between different systems, applications, or programming languages. 6 | 7 | :::tip 8 | Protobuf is a first-class citizen of kit. It is essential for you to master it. 9 | ::: 10 | 11 | We use it in these sections 12 | - [Cache](./cache) 13 | - [Configuration](./configuration) 14 | - [BackgroundJob](./background-job) 15 | - Api 16 | - RPC 17 | 18 | ## Grpc 19 | 20 | ## Grpc Gateway 21 | 22 | ## [Buf](https://buf.build/) 23 | 24 | ## Tools 25 | 26 | ### protoc-gen-go-errors-i18n 27 | 28 | Generate localizable errors 29 | 30 | ### protoc-gen-go-grpc-proxy 31 | 32 | Unify the grpc server and client interface -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/19-realtime.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Realtime 3 | title: Realtime 4 | --- 5 | 6 | Realtime functions of Kit is powered by [centrifuge](https://github.com/centrifugal/centrifuge) -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/20-email-sms.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Email & SMS 3 | title: Email & SMS 4 | --- -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/21-localization.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Localization 3 | title: Localization 4 | --- 5 | 6 | Localization of Kit is powered by [go-saas/go-i18n](https://github.com/go-saas/go-i18n), which is a forked version of [nicksnyder/go-i18n](https://github.com/nicksnyder/go-i18n) 7 | 8 | ## Middleware 9 | 10 | ```go 11 | localize.I18N() 12 | ``` -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/31-observability/01-logging.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Logging 3 | title: Logging 4 | --- -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/31-observability/02-tracing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Tracing 3 | title: Tracing 4 | --- -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/31-observability/03-metrics.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Metrics 3 | title: Metrics 4 | --- -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/31-observability/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Observability" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/docs/02-learn/01-fundamentals/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Fundamentals" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/docs/02-learn/01-web-basic.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Web Basic 3 | --- -------------------------------------------------------------------------------- /docs/docs/02-learn/02-security/01-csrf.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: CSRF 3 | title: CSRF 4 | --- -------------------------------------------------------------------------------- /docs/docs/02-learn/02-security/02-cors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: CORS 3 | title: CORS 4 | --- -------------------------------------------------------------------------------- /docs/docs/02-learn/02-security/03-logging.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Logging 3 | title: Logging 4 | --- 5 | 6 | ## Mask Logging 7 | 8 | implement `StringWithMask` to hide sensitive information like password in your logging 9 | ```go 10 | type StringWithMask interface { 11 | StringWithMask(mask string) string 12 | } 13 | ``` -------------------------------------------------------------------------------- /docs/docs/02-learn/02-security/04-ssl.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: SSL 3 | title: SSL 4 | --- 5 | 6 | https://apisix.apache.org/docs/apisix/certificate/ -------------------------------------------------------------------------------- /docs/docs/02-learn/02-security/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Security" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/docs/02-learn/03-frontend/01-openapi.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: OpenAPI 3 | title: OpenAPI 4 | --- 5 | 6 | https://github.com/go-saas/kit/tree/main/openapi -------------------------------------------------------------------------------- /docs/docs/02-learn/03-frontend/02-spa.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Single Page Application 3 | title: Single Page Application 4 | --- -------------------------------------------------------------------------------- /docs/docs/02-learn/03-frontend/03-react.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: React 3 | title: React 4 | --- -------------------------------------------------------------------------------- /docs/docs/02-learn/03-frontend/04-flutter.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Flutter 3 | title: Flutter 4 | --- -------------------------------------------------------------------------------- /docs/docs/02-learn/03-frontend/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Frontend" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/docs/02-learn/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Learn" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/docs/03-modules/01-user.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: User 3 | title: User 4 | --- -------------------------------------------------------------------------------- /docs/docs/03-modules/02-sys.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Sys 3 | title: Sys 4 | --- -------------------------------------------------------------------------------- /docs/docs/03-modules/03-saas.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Saas 3 | title: Saas 4 | --- -------------------------------------------------------------------------------- /docs/docs/03-modules/04-realtime.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Realtime 3 | title: Realtime 4 | --- -------------------------------------------------------------------------------- /docs/docs/03-modules/05-event.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Event 3 | title: Event 4 | --- -------------------------------------------------------------------------------- /docs/docs/03-modules/06-dtm.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Dtm 3 | title: Dtm 4 | --- -------------------------------------------------------------------------------- /docs/docs/03-modules/07-oidc.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Oidc 3 | title: Oidc 4 | --- -------------------------------------------------------------------------------- /docs/docs/03-modules/100-gateway.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Gateway 3 | title: Gateway 4 | --- -------------------------------------------------------------------------------- /docs/docs/03-modules/200-customize.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Customize 3 | title: Customize 4 | --- -------------------------------------------------------------------------------- /docs/docs/03-modules/201-create-your-own.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Create Your Own 3 | title: Create Your Own 4 | --- -------------------------------------------------------------------------------- /docs/docs/03-modules/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Modules" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/docs/04-develop/01-source codes.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/docs/04-develop/01-source codes.mdx -------------------------------------------------------------------------------- /docs/docs/04-develop/02-layer.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Layered Structure 3 | --- 4 | 5 | ## Private 6 | 7 | ## Sharable 8 | -------------------------------------------------------------------------------- /docs/docs/04-develop/03-api.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/docs/04-develop/03-api.mdx -------------------------------------------------------------------------------- /docs/docs/04-develop/04-cache.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/docs/04-develop/04-cache.mdx -------------------------------------------------------------------------------- /docs/docs/04-develop/05-event.mdx: -------------------------------------------------------------------------------- 1 | ## Kafka 2 | 3 | ## Plusar -------------------------------------------------------------------------------- /docs/docs/04-develop/06-i18n.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/docs/04-develop/06-i18n.mdx -------------------------------------------------------------------------------- /docs/docs/04-develop/07-permission.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/docs/04-develop/07-permission.mdx -------------------------------------------------------------------------------- /docs/docs/04-develop/08-menu.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/docs/04-develop/08-menu.mdx -------------------------------------------------------------------------------- /docs/docs/04-develop/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Develop" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/docs/05-deploy/01-local development.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/docs/05-deploy/01-local development.mdx -------------------------------------------------------------------------------- /docs/docs/05-deploy/02-k8s.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/docs/05-deploy/02-k8s.mdx -------------------------------------------------------------------------------- /docs/docs/05-deploy/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Deploy" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/docs/06-faq/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "FAQ" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/docs/07-roadmap/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Roadmap" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/docs/08-changelog/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Changelog" 2 | link: 3 | type: generated-index 4 | -------------------------------------------------------------------------------- /docs/en-US/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/en-US/overview.png -------------------------------------------------------------------------------- /docs/i18n/code.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current.json: -------------------------------------------------------------------------------- 1 | { 2 | "version.label": { 3 | "message": "下一步", 4 | "description": " " 5 | }, 6 | "sidebar.docs.category.Introduction": { 7 | "message": "简介", 8 | "description": " " 9 | } 10 | } -------------------------------------------------------------------------------- /docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/01-intro/02-architecture.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Architecture 3 | --- -------------------------------------------------------------------------------- /docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/01-intro/03-frontend.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Frontend 3 | --- -------------------------------------------------------------------------------- /docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/01-intro/04-develop.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_label: Develop your own services 3 | --- -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{ type: "autogenerated", dirName: "." }], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | module.exports = sidebars; 34 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | 25 | .buttonsItems { 26 | margin: 10px; 27 | } -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/acl.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/acl.drawio.png -------------------------------------------------------------------------------- /docs/static/img/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/admin.png -------------------------------------------------------------------------------- /docs/static/img/background-job-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/background-job-ui.png -------------------------------------------------------------------------------- /docs/static/img/backgroundjob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/backgroundjob.png -------------------------------------------------------------------------------- /docs/static/img/business-module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/business-module.png -------------------------------------------------------------------------------- /docs/static/img/customizable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/customizable.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/go-saas-kit.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/go-saas-kit.drawio.png -------------------------------------------------------------------------------- /docs/static/img/localization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/localization.png -------------------------------------------------------------------------------- /docs/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/logo.png -------------------------------------------------------------------------------- /docs/static/img/microservice.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/microservice.drawio.png -------------------------------------------------------------------------------- /docs/static/img/microservice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/microservice.png -------------------------------------------------------------------------------- /docs/static/img/minimal-module-design.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/minimal-module-design.drawio.png -------------------------------------------------------------------------------- /docs/static/img/modularity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/modularity.png -------------------------------------------------------------------------------- /docs/static/img/monolithic.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/monolithic.drawio.png -------------------------------------------------------------------------------- /docs/static/img/multi-tenancy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/multi-tenancy.png -------------------------------------------------------------------------------- /docs/static/img/realtime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/realtime.png -------------------------------------------------------------------------------- /docs/static/img/role-permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/static/img/role-permission.png -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docs/zh-CN/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/docs/zh-CN/overview.png -------------------------------------------------------------------------------- /dtm/README.md: -------------------------------------------------------------------------------- 1 | # DTM 2 | 3 | This is an embed service -------------------------------------------------------------------------------- /dtm/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | sapi "github.com/go-saas/kit/pkg/api" 7 | "github.com/go-saas/kit/pkg/authn/jwt" 8 | "github.com/go-saas/kit/pkg/conf" 9 | ) 10 | 11 | const ServiceName = "dtmservice" 12 | 13 | var ClientConf = &conf.Client{ 14 | ClientId: ServiceName, 15 | } 16 | 17 | func MustAddBranchHeader(ctx context.Context, tokenMgr sapi.TokenManager) map[string]string { 18 | t, err := tokenMgr.GetOrGenerateToken(ctx, ClientConf) 19 | if err != nil { 20 | panic(err) 21 | } 22 | return map[string]string{ 23 | jwt.AuthorizationHeader: fmt.Sprintf("%s %s", jwt.BearerTokenType, t), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /dtm/api/dtm/v1/dtm.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package dtm.api.dtm.v1; 4 | import "errors/errors.proto"; 5 | import "google/protobuf/empty.proto"; 6 | import "google/api/annotations.proto"; 7 | 8 | option go_package = "github.com/go-saas/kit/dtm/api/dtm/v1;v1"; 9 | 10 | service MsgService { 11 | rpc QueryPrepared(QueryPreparedRequest) returns (google.protobuf.Empty){ 12 | option (google.api.http) = { 13 | get: "/v1/{service}/dtm/query-prepared" 14 | }; 15 | }; 16 | } 17 | 18 | message QueryPreparedRequest{ 19 | string service=1; 20 | } -------------------------------------------------------------------------------- /dtm/current_version.txt: -------------------------------------------------------------------------------- 1 | v1.17.8 -------------------------------------------------------------------------------- /dtm/data/data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/pkg/dal" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | func GetDb(ctx context.Context, provider dal.ConstDbProvider, connName dal.ConnName) *gorm.DB { 10 | return provider.Get(ctx, string(connName)) 11 | } 12 | -------------------------------------------------------------------------------- /dtm/data/sqls/busi.mongo.js: -------------------------------------------------------------------------------- 1 | use dtm_busi 2 | db.user_account.drop() 3 | db.user_account.createIndex({ user_id: NumberLong(1) }, { unique: true }) 4 | 5 | db.user_account.insert({ user_id: NumberLong(1), balance: 10000 }) 6 | db.user_account.insert({ user_id: NumberLong(2), balance: 10000 }) 7 | -------------------------------------------------------------------------------- /dtm/data/sqls/busi.mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE if not exists dtm_busi 2 | /*!40100 DEFAULT CHARACTER SET utf8mb4 */ 3 | ; 4 | drop table if exists dtm_busi.user_account; 5 | create table if not exists dtm_busi.user_account( 6 | id int(11) PRIMARY KEY AUTO_INCREMENT, 7 | user_id int(11) UNIQUE, 8 | balance DECIMAL(10, 2) not null default '0', 9 | trading_balance DECIMAL(10, 2) not null default '0', 10 | create_time datetime DEFAULT now(), 11 | update_time datetime DEFAULT now(), 12 | key(create_time), 13 | key(update_time) 14 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; 15 | insert into dtm_busi.user_account (user_id, balance) 16 | values (1, 10000), 17 | (2, 10000) on DUPLICATE KEY 18 | UPDATE balance = 19 | values (balance); -------------------------------------------------------------------------------- /dtm/data/sqls/busi.postgres.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA if not exists dtm_busi 2 | /* SQLINES DEMO *** RACTER SET utf8mb4 */ 3 | ; 4 | drop table if exists dtm_busi.user_account; 5 | -- SQLINES LICENSE FOR EVALUATION USE ONLY 6 | create sequence if not exists dtm_busi.user_account_seq; 7 | create table if not exists dtm_busi.user_account( 8 | id int PRIMARY KEY DEFAULT NEXTVAL ('dtm_busi.user_account_seq'), 9 | user_id int UNIQUE, 10 | balance DECIMAL(10, 2) not null default '0', 11 | trading_balance DECIMAL(10, 2) not null default '0', 12 | create_time timestamp(0) with time zone DEFAULT now(), 13 | update_time timestamp(0) with time zone DEFAULT now() 14 | ); 15 | -- SQLINES LICENSE FOR EVALUATION USE ONLY 16 | create index if not exists create_idx on dtm_busi.user_account(create_time); 17 | -- SQLINES LICENSE FOR EVALUATION USE ONLY 18 | create index if not exists update_idx on dtm_busi.user_account(update_time); 19 | TRUNCATE dtm_busi.user_account; 20 | insert into dtm_busi.user_account (user_id, balance) 21 | values (1, 10000), 22 | (2, 10000); -------------------------------------------------------------------------------- /dtm/data/sqls/dtmcli.barrier.mongo.js: -------------------------------------------------------------------------------- 1 | use dtm_barrier 2 | db.barrier.drop() 3 | db.barrier.createIndex({ gid: 1, branch_id: 1, op: 1, barrier_id: 1 }, { unique: true }) 4 | db.barrier.insert({ gid: "123", branch_id: "01", op: "action", barrier_id: "01", reason: "test" }); 5 | -------------------------------------------------------------------------------- /dtm/data/sqls/dtmcli.barrier.mysql.sql: -------------------------------------------------------------------------------- 1 | create database if not exists dtm_barrier 2 | /*!40100 DEFAULT CHARACTER SET utf8mb4 */ 3 | ; 4 | drop table if exists dtm_barrier.barrier; 5 | create table if not exists dtm_barrier.barrier( 6 | id bigint(22) PRIMARY KEY AUTO_INCREMENT, 7 | trans_type varchar(45) default '', 8 | gid varchar(128) default '', 9 | branch_id varchar(128) default '', 10 | op varchar(45) default '', 11 | barrier_id varchar(45) default '', 12 | reason varchar(45) default '' comment 'the branch type who insert this record', 13 | create_time datetime DEFAULT now(), 14 | update_time datetime DEFAULT now(), 15 | key(create_time), 16 | key(update_time), 17 | UNIQUE key(gid, branch_id, op, barrier_id) 18 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -------------------------------------------------------------------------------- /dtm/data/sqls/dtmcli.barrier.postgres.sql: -------------------------------------------------------------------------------- 1 | create schema if not exists dtm_barrier; 2 | drop table if exists dtm_barrier.barrier; 3 | CREATE SEQUENCE if not EXISTS dtm_barrier.barrier_seq; 4 | create table if not exists dtm_barrier.barrier( 5 | id bigint NOT NULL DEFAULT NEXTVAL ('dtm_barrier.barrier_seq'), 6 | trans_type varchar(45) default '', 7 | gid varchar(128) default '', 8 | branch_id varchar(128) default '', 9 | op varchar(45) default '', 10 | barrier_id varchar(45) default '', 11 | reason varchar(45) default '', 12 | create_time timestamp(0) with time zone DEFAULT NULL, 13 | update_time timestamp(0) with time zone DEFAULT NULL, 14 | PRIMARY KEY(id), 15 | CONSTRAINT uniq_barrier unique(gid, branch_id, op, barrier_id) 16 | ); -------------------------------------------------------------------------------- /dtm/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/v2/middleware" 5 | "github.com/go-kratos/kratos/v2/transport/grpc" 6 | "github.com/go-kratos/kratos/v2/transport/http" 7 | v1 "github.com/go-saas/kit/dtm/api/dtm/v1" 8 | kitgrpc "github.com/go-saas/kit/pkg/server/grpc" 9 | kithttp "github.com/go-saas/kit/pkg/server/http" 10 | ) 11 | 12 | func NewHttpServerRegister(msg *MsgService) kithttp.ServiceRegister { 13 | return kithttp.ServiceRegisterFunc(func(server *http.Server, middleware ...middleware.Middleware) { 14 | v1.RegisterMsgServiceHTTPServer(server, msg) 15 | }) 16 | } 17 | func NewGrpcServerRegister(msg *MsgService) kitgrpc.ServiceRegister { 18 | return kitgrpc.ServiceRegisterFunc(func(server *grpc.Server, middleware ...middleware.Middleware) { 19 | v1.RegisterMsgServiceServer(server, msg) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /event/README.md: -------------------------------------------------------------------------------- 1 | # Event 2 | This is an embedded service -------------------------------------------------------------------------------- /event/api/v1/event.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/go-saas/kit/event" 5 | ) 6 | 7 | func (x *MessageProto) ToEvent() event.Event { 8 | ret := event.NewMessage(x.Key, x.Value) 9 | for k, v := range x.Header { 10 | ret.Header().Set(k, v) 11 | } 12 | return ret 13 | } 14 | 15 | func MessageProtoFromEvent(e event.Event) *MessageProto { 16 | ret := &MessageProto{ 17 | Key: e.Key(), 18 | Value: e.Value(), 19 | Header: map[string]string{}, 20 | } 21 | 22 | for _, s := range e.Header().Keys() { 23 | ret.Header[s] = e.Header().Get(s) 24 | } 25 | return ret 26 | } 27 | -------------------------------------------------------------------------------- /event/api/v1/event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package event.api.v1; 4 | import "errors/errors.proto"; 5 | import "google/protobuf/empty.proto"; 6 | import "google/api/annotations.proto"; 7 | import "validate/validate.proto"; 8 | 9 | option go_package = "github.com/go-saas/kit/event/api/v1;v1"; 10 | 11 | 12 | message MessageProto{ 13 | string key=1; 14 | bytes value=2; 15 | map header=3; 16 | } 17 | 18 | message HandleEventRequest{ 19 | MessageProto message=2 [(validate.rules).message.required=true]; 20 | } 21 | 22 | message PublishEventRequest{ 23 | repeated MessageProto messages=2 [(validate.rules).repeated.min_items=1]; 24 | } 25 | 26 | // EventService proxy event handler 27 | service EventService{ 28 | rpc HandleEvent(HandleEventRequest)returns(google.protobuf.Empty){ 29 | } 30 | rpc PublishEvent(PublishEventRequest)returns(google.protobuf.Empty){ 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /event/context.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import "context" 4 | 5 | type ( 6 | consumerKey struct{} 7 | producerKey struct{} 8 | ) 9 | 10 | func NewConsumerContext(ctx context.Context, r Consumer) context.Context { 11 | return context.WithValue(ctx, consumerKey{}, r) 12 | } 13 | 14 | func FromConsumerContext(ctx context.Context) (Consumer, bool) { 15 | v, ok := ctx.Value(consumerKey{}).(Consumer) 16 | return v, ok 17 | } 18 | func NewProducerContext(ctx context.Context, r Producer) context.Context { 19 | return context.WithValue(ctx, producerKey{}, r) 20 | } 21 | 22 | func FromProducerContext(ctx context.Context) (Producer, bool) { 23 | v, ok := ctx.Value(producerKey{}).(Producer) 24 | return v, ok 25 | } 26 | -------------------------------------------------------------------------------- /event/event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package event; 4 | 5 | import "google/protobuf/duration.proto"; 6 | import "google/protobuf/struct.proto"; 7 | import "google/protobuf/wrappers.proto"; 8 | 9 | option go_package = "github.com/go-saas/kit/event;event"; 10 | 11 | message Config{ 12 | string type=1; 13 | string addr=2; 14 | string topic=3; 15 | string group=4; 16 | 17 | message Kafka{ 18 | optional google.protobuf.StringValue version=1; 19 | } 20 | 21 | Kafka kafka=10; 22 | 23 | message Pulsar{ 24 | optional google.protobuf.Duration operation_timeout=1; 25 | optional google.protobuf.Duration connection_timeout=2; 26 | } 27 | 28 | Pulsar pulsar=11; 29 | 30 | google.protobuf.Struct extra=100; 31 | } 32 | -------------------------------------------------------------------------------- /event/logging.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "context" 5 | klog "github.com/go-kratos/kratos/v2/log" 6 | ) 7 | 8 | // Logging logging errors 9 | func Logging(logger klog.Logger) ConsumerMiddlewareFunc { 10 | return func(next ConsumerHandler) ConsumerHandler { 11 | return ConsumerHandlerFunc(func(ctx context.Context, event Event) error { 12 | err := next.Process(ctx, event) 13 | if err != nil { 14 | _ = klog.WithContext(ctx, logger).Log(klog.LevelError, 15 | klog.DefaultMessageKey, err.Error(), 16 | "event", event.Key()) 17 | } else { 18 | _ = klog.WithContext(ctx, logger).Log(klog.LevelInfo, 19 | "event", event.Key()) 20 | } 21 | return err 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /event/proto.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "context" 5 | "google.golang.org/protobuf/proto" 6 | ) 7 | 8 | func NewMessageFromProto(msg proto.Message) (Event, error) { 9 | key, v, err := KVFromProto(msg) 10 | if err != nil { 11 | return nil, err 12 | } 13 | return NewMessage(key, v), nil 14 | } 15 | 16 | func KVFromProto(msg proto.Message) (string, []byte, error) { 17 | key := string(msg.ProtoReflect().Descriptor().FullName()) 18 | data, err := proto.Marshal(msg) 19 | if err != nil { 20 | return key, nil, err 21 | } 22 | return key, data, nil 23 | } 24 | 25 | func ProtoHandler[T proto.Message](msg T, h HandlerOf[T]) ConsumerHandler { 26 | key := string(msg.ProtoReflect().Descriptor().FullName()) 27 | return FilterKey(key, NewTransformer(func(_ context.Context, event Event) (T, error) { 28 | err := proto.Unmarshal(event.Value(), msg) 29 | return msg, err 30 | }, h)) 31 | } 32 | -------------------------------------------------------------------------------- /event/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | v12 "github.com/go-saas/kit/event/api/v1" 5 | "github.com/go-saas/kit/event/service" 6 | kitdi "github.com/go-saas/kit/pkg/di" 7 | "github.com/goava/di" 8 | ) 9 | 10 | var EventProviderSet = kitdi.NewSet( 11 | kitdi.NewProvider(service.NewEventService, di.As(new(v12.EventServiceServer))), 12 | service.NewGrpcServerRegister, service.NewHttpServerRegister, 13 | ) 14 | -------------------------------------------------------------------------------- /event/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/v2/middleware" 5 | "github.com/go-kratos/kratos/v2/transport/grpc" 6 | "github.com/go-kratos/kratos/v2/transport/http" 7 | v12 "github.com/go-saas/kit/event/api/v1" 8 | kitgrpc "github.com/go-saas/kit/pkg/server/grpc" 9 | kithttp "github.com/go-saas/kit/pkg/server/http" 10 | ) 11 | 12 | func NewHttpServerRegister(event *EventService) kithttp.ServiceRegister { 13 | return kithttp.ServiceRegisterFunc(func(server *http.Server, middleware ...middleware.Middleware) { 14 | 15 | }) 16 | } 17 | func NewGrpcServerRegister(event *EventService) kitgrpc.ServiceRegister { 18 | return kitgrpc.ServiceRegisterFunc(func(server *grpc.Server, middleware ...middleware.Middleware) { 19 | v12.RegisterEventServiceServer(server, event) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /event/uow.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/uow" 6 | ) 7 | 8 | //ConsumerUow wrap handler into a unit of work (transaction) 9 | func ConsumerUow(uowMgr uow.Manager) ConsumerMiddlewareFunc { 10 | return func(handler ConsumerHandler) ConsumerHandler { 11 | return ConsumerHandlerFunc(func(ctx context.Context, event Event) error { 12 | return uowMgr.WithNew(ctx, func(ctx context.Context) error { 13 | return handler.Process(ctx, event) 14 | }) 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gateway/apisix/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20 AS builder 2 | 3 | COPY . /src 4 | WORKDIR /src/gateway/apisix 5 | 6 | RUN make -f ./Makefile build 7 | 8 | 9 | FROM apache/apisix:3.6.0-redhat 10 | 11 | COPY --from=builder /src/gateway/apisix/bin /app 12 | -------------------------------------------------------------------------------- /gateway/apisix/Makefile: -------------------------------------------------------------------------------- 1 | GOPATH:=$(shell go env GOPATH) 2 | VERSION=$(shell git describe --tags --always) 3 | 4 | .PHONY: generate 5 | # generate 6 | generate: 7 | go generate ./... 8 | 9 | .PHONY: build 10 | # build 11 | build: 12 | mkdir -p bin/ && CGO_ENABLED=0 go build -buildvcs=false -ldflags "-X main.Version=$(VERSION)" -o ./bin/ ./... 13 | 14 | .PHONY: all 15 | all: 16 | go mod tidy 17 | make generate 18 | 19 | # show help 20 | help: 21 | @echo '' 22 | @echo 'Usage:' 23 | @echo ' make [target]' 24 | @echo '' 25 | @echo 'Targets:' 26 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 27 | helpMessage = match(lastLine, /^# (.*)/); \ 28 | if (helpMessage) { \ 29 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 30 | helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \ 31 | printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \ 32 | } \ 33 | } \ 34 | { lastLine = $$0 }' $(MAKEFILE_LIST) 35 | 36 | .DEFAULT_GOAL := help 37 | -------------------------------------------------------------------------------- /gateway/apisix/internal/conf/conf.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package apisix.internal; 3 | 4 | option go_package = "github.com/go-saas/kit/gateway/apisix/internal/conf;conf"; 5 | 6 | import "conf/conf.proto"; 7 | 8 | message Bootstrap { 9 | conf.Security security=3; 10 | conf.Services services =4; 11 | conf.Logging logging=5; 12 | conf.Tracers tracing=7; 13 | conf.AppConfig app=8; 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /oidc/README.md: -------------------------------------------------------------------------------- 1 | # OpenID Connect 2 | 3 | This is an embed service 4 | -------------------------------------------------------------------------------- /oidc/api/permission.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/authz/authz" 6 | ) 7 | 8 | const ( 9 | ResourceClient = "oidc.client" 10 | ResourceKey = "oidc.key" 11 | ) 12 | 13 | //go:embed permission.yaml 14 | var permission []byte 15 | 16 | func init() { 17 | authz.LoadFromYaml(permission) 18 | } 19 | -------------------------------------------------------------------------------- /oidc/data/data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | kconf "github.com/go-saas/kit/pkg/conf" 5 | kitdi "github.com/go-saas/kit/pkg/di" 6 | client "github.com/ory/hydra-client-go/v2" 7 | ) 8 | 9 | var ProviderSet = kitdi.NewSet( 10 | NewHydra, 11 | ) 12 | 13 | func NewHydra(c *kconf.Security) *client.APIClient { 14 | cfg := client.NewConfiguration() 15 | cfg.Servers = client.ServerConfigurations{ 16 | { 17 | URL: c.Oidc.Hydra.AdminUrl, 18 | }, 19 | } 20 | return client.NewAPIClient(cfg) 21 | } 22 | -------------------------------------------------------------------------------- /oidc/i18n/embed/en-US.toml: -------------------------------------------------------------------------------- 1 | 2 | [permission.oidc.client.group] 3 | other = "Client" 4 | [permission.oidc.client.read] 5 | other = "Read Client" 6 | [permission.oidc.client.create] 7 | other = "Create Client" 8 | [permission.oidc.client.update] 9 | other = "Update Client" 10 | [permission.oidc.client.delete] 11 | other = "Delete Client" 12 | 13 | [permission.oidc.key.group] 14 | other = "Key" 15 | [permission.oidc.key.read] 16 | other = "Read Key" 17 | [permission.oidc.key.create] 18 | other = "Create Key" 19 | [permission.oidc.key.update] 20 | other = "Update Key" 21 | [permission.oidc.key.delete] 22 | other = "Delete Key" 23 | 24 | [oidc.title] 25 | other = "OpenID Connect" 26 | [oidc.client.management] 27 | other = "Client Management" 28 | [oidc.keys.management] 29 | other = "Key Management" -------------------------------------------------------------------------------- /oidc/i18n/embed/zh-CN.toml: -------------------------------------------------------------------------------- 1 | [permission.oidc.client.group] 2 | other = "客户端" 3 | [permission.oidc.client.read] 4 | other = "查看客户端" 5 | [permission.oidc.client.create] 6 | other = "新增客户端" 7 | [permission.oidc.client.update] 8 | other = "修改客户端" 9 | [permission.oidc.client.delete] 10 | other = "删除客户端" 11 | 12 | [permission.oidc.key.group] 13 | other = "密钥" 14 | [permission.oidc.key.read] 15 | other = "查看密钥" 16 | [permission.oidc.key.create] 17 | other = "新增密钥" 18 | [permission.oidc.key.update] 19 | other = "修改密钥" 20 | [permission.oidc.key.delete] 21 | other = "删除密钥" 22 | 23 | [oidc.title] 24 | other = "OpenID Connect" 25 | [oidc.client.management] 26 | other = "客户端管理" 27 | [oidc.keys.management] 28 | other = "密钥管理" -------------------------------------------------------------------------------- /oidc/i18n/i18n.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "embed" 5 | "github.com/go-saas/kit/pkg/localize" 6 | ) 7 | 8 | var ( 9 | //go:embed embed 10 | f embed.FS 11 | ) 12 | 13 | func init() { 14 | localize.RegisterFileBundle(localize.FileBundle{Fs: f}) 15 | } 16 | -------------------------------------------------------------------------------- /openapi/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM swaggerapi/swagger-ui:latest 2 | COPY ./openapi /usr/share/nginx/html/api 3 | ENV URLS="[{url: 'api/kit-merged.swagger.json', name: 'kit-merged'},{url: '/v1/user/dev/swagger/swagger_spec', name: 'user'},{url: '/v1/saas/dev/swagger/swagger_spec', name: 'saas'},{url: '/v1/sys/dev/swagger/swagger_spec', name: 'sys'},{url: '/v1/realtime/dev/swagger/swagger_spec', name: 'realtime'},{url: '/v1/product/dev/swagger/swagger_spec', name: 'product'},{url: '/v1/payment/dev/swagger/swagger_spec', name: 'payment'},{url: '/v1/order/dev/swagger/swagger_spec', name: 'order'}]" 4 | EXPOSE 8080 -------------------------------------------------------------------------------- /order/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 AS builder 2 | 3 | COPY . /src 4 | WORKDIR /src 5 | 6 | RUN make -f ./order/Makefile build 7 | 8 | FROM debian:stable-slim 9 | 10 | RUN apt-get update && apt-get install -y --no-install-recommends \ 11 | ca-certificates \ 12 | netbase \ 13 | && rm -rf /var/lib/apt/lists/ \ 14 | && apt-get autoremove -y && apt-get autoclean -y 15 | 16 | COPY --from=builder /src/bin /app 17 | 18 | WORKDIR /app 19 | 20 | EXPOSE 8000 21 | EXPOSE 9000 22 | VOLUME /data/conf 23 | 24 | CMD ["./order", "-conf", "/data/conf"] 25 | -------------------------------------------------------------------------------- /order/Makefile: -------------------------------------------------------------------------------- 1 | GOPATH:=$(shell go env GOPATH) 2 | VERSION=$(shell git describe --tags --always) 3 | 4 | 5 | .PHONY: build 6 | # build 7 | build: 8 | mkdir -p bin/ && go build -buildvcs=false -ldflags "-X main.Version=$(VERSION)" -o ./bin/ ./... 9 | 10 | .PHONY: api 11 | # generate api proto 12 | api: 13 | cd .. && buf generate --path ./buf/order --template ./order/buf.gen.yaml 14 | 15 | .PHONY: all 16 | # generate all 17 | all: 18 | make api; 19 | 20 | 21 | # show help 22 | help: 23 | @echo '' 24 | @echo 'Usage:' 25 | @echo ' make [target]' 26 | @echo '' 27 | @echo 'Targets:' 28 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 29 | helpMessage = match(lastLine, /^# (.*)/); \ 30 | if (helpMessage) { \ 31 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 32 | helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \ 33 | printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \ 34 | } \ 35 | } \ 36 | { lastLine = $$0 }' $(MAKEFILE_LIST) 37 | 38 | .DEFAULT_GOAL := help 39 | -------------------------------------------------------------------------------- /order/README.md: -------------------------------------------------------------------------------- 1 | # Order -------------------------------------------------------------------------------- /order/api/gateway.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/apisix" 6 | ) 7 | 8 | //go:embed gateway.yaml 9 | var gateway []byte 10 | 11 | func init() { 12 | apisix.LoadFromYaml(gateway) 13 | } 14 | -------------------------------------------------------------------------------- /order/api/gateway.yaml: -------------------------------------------------------------------------------- 1 | routes: 2 | order-api: 3 | uris: ["/v1/order*","/assets/order/*"] 4 | upstream_id: order-http 5 | -------------------------------------------------------------------------------- /order/api/order/v1/error_reason.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: order/api/order/v1/error_reason.proto 3 | 4 | package v1 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | 19 | "google.golang.org/protobuf/types/known/anypb" 20 | ) 21 | 22 | // ensure the imports are used 23 | var ( 24 | _ = bytes.MinRead 25 | _ = errors.New("") 26 | _ = fmt.Print 27 | _ = utf8.UTFMax 28 | _ = (*regexp.Regexp)(nil) 29 | _ = (*strings.Reader)(nil) 30 | _ = net.IPv4len 31 | _ = time.Duration(0) 32 | _ = (*url.URL)(nil) 33 | _ = (*mail.Address)(nil) 34 | _ = anypb.Any{} 35 | _ = sort.Sort 36 | ) 37 | -------------------------------------------------------------------------------- /order/api/order/v1/error_reason.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package order.api.order.v1; 4 | import "errors/errors.proto"; 5 | 6 | option go_package = "github.com/go-saas/kit/order/api/post/v1;v1"; 7 | 8 | enum ErrorReason { 9 | option (errors.default_code) = 500; 10 | 11 | CONTENT_MISSING = 0 [(errors.code) = 400]; 12 | } 13 | -------------------------------------------------------------------------------- /order/api/permission.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/authz/authz" 6 | ) 7 | 8 | const ( 9 | ResourceOrder = "order.order" 10 | ) 11 | 12 | //go:embed permission.yaml 13 | var permission []byte 14 | 15 | func init() { 16 | authz.LoadFromYaml(permission) 17 | } 18 | -------------------------------------------------------------------------------- /order/api/permission.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: "permission.order.order.group" 3 | def: 4 | - name: "permission.order.order.any" 5 | namespace: "order.order" 6 | action: "*" 7 | internal: true 8 | - name: "permission.permission.read" 9 | namespace: "order.order" 10 | action: "read" 11 | - name: "permission.permission.write" 12 | namespace: "order.order" 13 | action: "write" 14 | -------------------------------------------------------------------------------- /order/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: buf.build/grpc-ecosystem/openapiv2:v2.15.2 4 | out: ./order/private/service/openapi 5 | opt: 6 | - allow_merge=true 7 | - proto3_optional_nullable=true 8 | - merge_file_name=api -------------------------------------------------------------------------------- /order/event/v1/event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package order.event; 3 | 4 | import "order/api/order/v1/order.proto"; 5 | 6 | option go_package = "github.com/go-saas/kit/order/event/v1;v1"; 7 | 8 | message OrderPaySuccessEvent{ 9 | .order.api.order.v1.Order order=1; 10 | } 11 | 12 | message OrderRefundSuccessEvent{ 13 | .order.api.order.v1.Order order=1; 14 | } 15 | -------------------------------------------------------------------------------- /order/i18n/embed/en-US.toml: -------------------------------------------------------------------------------- 1 | [permission.order.order.group] 2 | other = "Order" 3 | [permission.order.order.read] 4 | other = "Read Order" 5 | [permission.order.order.write] 6 | other = "Write Order" 7 | 8 | [order.manager] 9 | other = "Order Management" 10 | [order.order] 11 | other = "Order List" -------------------------------------------------------------------------------- /order/i18n/embed/zh-CN.toml: -------------------------------------------------------------------------------- 1 | [permission.order.order.group] 2 | other = "订单" 3 | [permission.order.order.read] 4 | other = "查看订单" 5 | [permission.order.order.write] 6 | other = "增加/修改/删除订单" 7 | 8 | [order.manager] 9 | other = "订单管理" 10 | [order.order] 11 | other = "订单列表" -------------------------------------------------------------------------------- /order/i18n/i18n.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "embed" 5 | "github.com/go-saas/kit/pkg/localize" 6 | ) 7 | 8 | var ( 9 | //go:embed embed 10 | f embed.FS 11 | ) 12 | 13 | func init() { 14 | localize.RegisterFileBundle(localize.FileBundle{Fs: f}) 15 | } 16 | -------------------------------------------------------------------------------- /order/private/biz/README.md: -------------------------------------------------------------------------------- 1 | # Biz 2 | -------------------------------------------------------------------------------- /order/private/biz/biz.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/go-saas/kit/pkg/dal" 5 | kitdi "github.com/go-saas/kit/pkg/di" 6 | ) 7 | 8 | // ProviderSet is biz providers. 9 | var ProviderSet = kitdi.NewSet(NewPostSeeder) 10 | 11 | const ConnName dal.ConnName = "order" 12 | -------------------------------------------------------------------------------- /order/private/biz/seed.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/saas/seed" 6 | ) 7 | 8 | type PostSeeder struct { 9 | } 10 | 11 | var _ seed.Contrib = (*PostSeeder)(nil) 12 | 13 | func NewPostSeeder() *PostSeeder { 14 | return &PostSeeder{} 15 | } 16 | 17 | func (p *PostSeeder) Seed(ctx context.Context, sCtx *seed.Context) error { 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /order/private/conf/conf.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package order.private.conf; 3 | 4 | option go_package = "github.com/go-saas/kit/order/private/conf;conf"; 5 | 6 | import "conf/conf.proto"; 7 | import "blob/blob.proto"; 8 | 9 | message Bootstrap { 10 | .conf.Data data = 2; 11 | .conf.Security security=3; 12 | .conf.Services services =4; 13 | .conf.Logging logging=6; 14 | .conf.Tracers tracing=7; 15 | .conf.AppConfig app=8; 16 | } 17 | 18 | 19 | -------------------------------------------------------------------------------- /order/private/data/README.md: -------------------------------------------------------------------------------- 1 | # Data 2 | -------------------------------------------------------------------------------- /order/private/data/db_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "gorm.io/driver/sqlite" 6 | "gorm.io/gorm" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | var db *gorm.DB 12 | 13 | func TestMain(m *testing.M) { 14 | var err error 15 | db, err = gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) 16 | if err != nil { 17 | panic(err) 18 | } 19 | db = db.Debug() 20 | 21 | exitCode := m.Run() 22 | os.Exit(exitCode) 23 | } 24 | 25 | func TestMigration(t *testing.T) { 26 | err := migrateDb(db) 27 | assert.NoError(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /order/private/data/migrate.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/order/private/biz" 6 | kitgorm "github.com/go-saas/kit/pkg/gorm" 7 | "github.com/go-saas/saas/seed" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type Migrate struct { 12 | data *Data 13 | } 14 | 15 | func NewMigrate(data *Data) *Migrate { 16 | return &Migrate{ 17 | data: data, 18 | } 19 | } 20 | func (m *Migrate) Seed(ctx context.Context, sCtx *seed.Context) error { 21 | //make sure database exists 22 | ctx = kitgorm.NewDbGuardianContext(ctx) 23 | db := GetDb(ctx, m.data.DbProvider) 24 | return migrateDb(db) 25 | } 26 | 27 | func migrateDb(db *gorm.DB) error { 28 | return db.AutoMigrate(&biz.Order{}, &biz.OrderItem{}, &biz.OrderPaymentProvider{}) 29 | } 30 | -------------------------------------------------------------------------------- /order/private/server/event.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | klog "github.com/go-kratos/kratos/v2/log" 5 | "github.com/go-saas/kit/event" 6 | "github.com/go-saas/kit/event/trace" 7 | kitconf "github.com/go-saas/kit/pkg/conf" 8 | "github.com/go-saas/kit/pkg/dal" 9 | uow2 "github.com/go-saas/uow" 10 | "github.com/goava/di" 11 | ) 12 | 13 | func NewEventServer( 14 | c *kitconf.Data, 15 | conn dal.ConnName, 16 | logger klog.Logger, 17 | uowMgr uow2.Manager, 18 | handlers []event.ConsumerHandler, 19 | container *di.Container, 20 | ) *event.ConsumerFactoryServer { 21 | e := c.Endpoints.GetEventMergedDefault(string(conn)) 22 | srv := event.NewConsumerFactoryServer(e, container) 23 | srv.Use(event.ConsumerRecover(event.WithLogger(logger)), trace.Receive(), event.Logging(logger), event.ConsumerUow(uowMgr)) 24 | for _, handler := range handlers { 25 | srv.Append(handler) 26 | } 27 | return srv 28 | } 29 | -------------------------------------------------------------------------------- /order/private/service/README.md: -------------------------------------------------------------------------------- 1 | # Service 2 | -------------------------------------------------------------------------------- /payment/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 AS builder 2 | 3 | COPY . /src 4 | WORKDIR /src 5 | 6 | RUN make -f ./payment/Makefile build 7 | 8 | FROM debian:stable-slim 9 | 10 | RUN apt-get update && apt-get install -y --no-install-recommends \ 11 | ca-certificates \ 12 | netbase \ 13 | && rm -rf /var/lib/apt/lists/ \ 14 | && apt-get autoremove -y && apt-get autoclean -y 15 | 16 | COPY --from=builder /src/bin /app 17 | 18 | WORKDIR /app 19 | 20 | EXPOSE 8000 21 | EXPOSE 9000 22 | VOLUME /data/conf 23 | 24 | CMD ["./payment", "-conf", "/data/conf"] 25 | -------------------------------------------------------------------------------- /payment/Makefile: -------------------------------------------------------------------------------- 1 | GOPATH:=$(shell go env GOPATH) 2 | VERSION=$(shell git describe --tags --always) 3 | 4 | 5 | .PHONY: build 6 | # build 7 | build: 8 | mkdir -p bin/ && go build -buildvcs=false -ldflags "-X main.Version=$(VERSION)" -o ./bin/ ./... 9 | 10 | .PHONY: api 11 | # generate api proto 12 | api: 13 | cd .. && buf generate --path ./buf/payment --template ./payment/buf.gen.yaml 14 | 15 | .PHONY: all 16 | # generate all 17 | all: 18 | make api; 19 | 20 | 21 | # show help 22 | help: 23 | @echo '' 24 | @echo 'Usage:' 25 | @echo ' make [target]' 26 | @echo '' 27 | @echo 'Targets:' 28 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 29 | helpMessage = match(lastLine, /^# (.*)/); \ 30 | if (helpMessage) { \ 31 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 32 | helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \ 33 | printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \ 34 | } \ 35 | } \ 36 | { lastLine = $$0 }' $(MAKEFILE_LIST) 37 | 38 | .DEFAULT_GOAL := help 39 | -------------------------------------------------------------------------------- /payment/README.md: -------------------------------------------------------------------------------- 1 | # Payment -------------------------------------------------------------------------------- /payment/api/checkout/v1/error_reason.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: payment/api/checkout/v1/error_reason.proto 3 | 4 | package v1 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | 19 | "google.golang.org/protobuf/types/known/anypb" 20 | ) 21 | 22 | // ensure the imports are used 23 | var ( 24 | _ = bytes.MinRead 25 | _ = errors.New("") 26 | _ = fmt.Print 27 | _ = utf8.UTFMax 28 | _ = (*regexp.Regexp)(nil) 29 | _ = (*strings.Reader)(nil) 30 | _ = net.IPv4len 31 | _ = time.Duration(0) 32 | _ = (*url.URL)(nil) 33 | _ = (*mail.Address)(nil) 34 | _ = anypb.Any{} 35 | _ = sort.Sort 36 | ) 37 | -------------------------------------------------------------------------------- /payment/api/checkout/v1/error_reason.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package payment.api.checkout.v1; 4 | import "errors/errors.proto"; 5 | 6 | option go_package = "github.com/go-saas/kit/payment/api/checkout/v1;v1"; 7 | 8 | enum ErrorReason { 9 | 10 | option (errors.default_code) = 500; 11 | 12 | PRODUCT_ACROSS_TENANT = 0 [(errors.code) = 400]; 13 | PRICE_TYPE_UNSUPPORTED=1[(errors.code) = 400]; 14 | CURRENCY_UNSUPPORTED=2[(errors.code) = 400]; 15 | } -------------------------------------------------------------------------------- /payment/api/gateway.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/apisix" 6 | ) 7 | 8 | //go:embed gateway.yaml 9 | var gateway []byte 10 | 11 | func init() { 12 | apisix.LoadFromYaml(gateway) 13 | } 14 | -------------------------------------------------------------------------------- /payment/api/gateway.yaml: -------------------------------------------------------------------------------- 1 | routes: 2 | payment-api: 3 | uris: ["/v1/payment/*","/v1/subscription*","/assets/payment/*"] 4 | upstream_id: payment-http 5 | -------------------------------------------------------------------------------- /payment/api/permission.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/authz/authz" 6 | ) 7 | 8 | const ( 9 | ResourceSubscription = "payment.subscription" 10 | ) 11 | 12 | //go:embed permission.yaml 13 | var permission []byte 14 | 15 | func init() { 16 | authz.LoadFromYaml(permission) 17 | } 18 | -------------------------------------------------------------------------------- /payment/api/permission.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: "permission.payment.subscription.group" 3 | def: 4 | - name: "permission.payment.subscription.any" 5 | namespace: "payment.subscription" 6 | action: "*" 7 | internal: true 8 | - name: "permission.permission.read" 9 | namespace: "payment.subscription" 10 | action: "read" 11 | - name: "permission.permission.write" 12 | namespace: "payment.subscription" 13 | action: "write" 14 | -------------------------------------------------------------------------------- /payment/api/subscription/v1/error_reason.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: payment/api/subscription/v1/error_reason.proto 3 | 4 | package v1 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | 19 | "google.golang.org/protobuf/types/known/anypb" 20 | ) 21 | 22 | // ensure the imports are used 23 | var ( 24 | _ = bytes.MinRead 25 | _ = errors.New("") 26 | _ = fmt.Print 27 | _ = utf8.UTFMax 28 | _ = (*regexp.Regexp)(nil) 29 | _ = (*strings.Reader)(nil) 30 | _ = net.IPv4len 31 | _ = time.Duration(0) 32 | _ = (*url.URL)(nil) 33 | _ = (*mail.Address)(nil) 34 | _ = anypb.Any{} 35 | _ = sort.Sort 36 | ) 37 | -------------------------------------------------------------------------------- /payment/api/subscription/v1/error_reason.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package payment.api.subscription.v1; 4 | import "errors/errors.proto"; 5 | 6 | option go_package = "github.com/go-saas/kit/payment/api/subscription/v1;v1"; 7 | 8 | enum ErrorReason { 9 | 10 | option (errors.default_code) = 500; 11 | 12 | SUBSCRIPTION_NOT_FOUND=0[(errors.code) = 404]; 13 | } -------------------------------------------------------------------------------- /payment/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: buf.build/grpc-ecosystem/openapiv2:v2.15.2 4 | out: ./payment/private/service/openapi 5 | opt: 6 | - allow_merge=true 7 | - proto3_optional_nullable=true 8 | - merge_file_name=api -------------------------------------------------------------------------------- /payment/event/v1/event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package payment.event; 3 | 4 | option go_package = "github.com/go-saas/kit/payment/event/v1;v1"; 5 | 6 | message SubscriptionChangedEvent{ 7 | string id=1; 8 | } 9 | -------------------------------------------------------------------------------- /payment/i18n/embed/en-US.toml: -------------------------------------------------------------------------------- 1 | [payment.manager] 2 | other = "Payment Management" 3 | 4 | [payment.subscription] 5 | other = "Subscription List" 6 | 7 | [permission.payment.subscription.group] 8 | other = "Subscription" 9 | [permission.payment.subscription.read] 10 | other = "Read Subscription" 11 | [permission.payment.subscription.write] 12 | other = "Write Subscription" 13 | -------------------------------------------------------------------------------- /payment/i18n/embed/zh-CN.toml: -------------------------------------------------------------------------------- 1 | [payment.manager] 2 | other = "支付管理" 3 | 4 | [payment.subscription] 5 | other = "订阅管理" 6 | 7 | [permission.payment.subscription.group] 8 | other = "订阅" 9 | [permission.payment.subscription.read] 10 | other = "读取订阅" 11 | [permission.payment.subscription.write] 12 | other = "增加/修改/删除订阅" -------------------------------------------------------------------------------- /payment/i18n/i18n.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "embed" 5 | "github.com/go-saas/kit/pkg/localize" 6 | ) 7 | 8 | var ( 9 | //go:embed embed 10 | f embed.FS 11 | ) 12 | 13 | func init() { 14 | localize.RegisterFileBundle(localize.FileBundle{Fs: f}) 15 | } 16 | -------------------------------------------------------------------------------- /payment/menu/menu.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/sys/menu" 6 | ) 7 | 8 | //go:embed menu.yaml 9 | var menuData []byte 10 | 11 | func init() { 12 | menu.LoadFromYaml(menuData) 13 | } 14 | -------------------------------------------------------------------------------- /payment/menu/menu.yaml: -------------------------------------------------------------------------------- 1 | menus: 2 | - name: "payment" 3 | path: "/payment" 4 | component: "LAYOUT" 5 | redirect: "/payment/orders" 6 | is_preserved: true 7 | title: "payment.manager" 8 | icon: "DollarOutlined" 9 | meta: 10 | priority: 310 11 | children: 12 | - name: "order.order" 13 | icon: "ant-design:api-outlined" 14 | path: "/payment/orders" 15 | component: "/Order/Order" 16 | title: "order.order" 17 | requirement: 18 | - namespace: "order.order" 19 | resource: "*" 20 | action: "read" 21 | 22 | - name: "payment.subscription" 23 | icon: "ant-design:api-outlined" 24 | path: "/payment/subscriptions" 25 | component: "/Subscription/Subscription" 26 | title: "payment.subscription" 27 | requirement: 28 | - namespace: "payment.subscription" 29 | resource: "*" 30 | action: "read" 31 | -------------------------------------------------------------------------------- /payment/private/biz/README.md: -------------------------------------------------------------------------------- 1 | # Biz 2 | -------------------------------------------------------------------------------- /payment/private/biz/biz.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/go-saas/kit/pkg/dal" 5 | kitdi "github.com/go-saas/kit/pkg/di" 6 | ) 7 | 8 | // ProviderSet is biz providers. 9 | var ProviderSet = kitdi.NewSet(NewPostSeeder) 10 | 11 | const ConnName dal.ConnName = "payment" 12 | -------------------------------------------------------------------------------- /payment/private/biz/seed.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/saas/seed" 6 | ) 7 | 8 | type PostSeeder struct { 9 | } 10 | 11 | var _ seed.Contrib = (*PostSeeder)(nil) 12 | 13 | func NewPostSeeder() *PostSeeder { 14 | return &PostSeeder{} 15 | } 16 | 17 | func (p *PostSeeder) Seed(ctx context.Context, sCtx *seed.Context) error { 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /payment/private/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | const defaultKey = "default" 4 | -------------------------------------------------------------------------------- /payment/private/conf/conf.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package payment.private.conf; 3 | 4 | option go_package = "github.com/go-saas/kit/payment/private/conf;conf"; 5 | 6 | import "conf/conf.proto"; 7 | import "blob/blob.proto"; 8 | import "stripe/stripe.proto"; 9 | 10 | message Bootstrap { 11 | .conf.Data data = 2; 12 | .conf.Security security=3; 13 | .conf.Services services =4; 14 | .conf.Logging logging=6; 15 | .conf.Tracers tracing=7; 16 | .conf.AppConfig app=8; 17 | 18 | PaymentConf payment=500; 19 | 20 | .stripe.Conf stripe=501; 21 | } 22 | 23 | 24 | message PaymentConf{ 25 | 26 | } 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /payment/private/data/README.md: -------------------------------------------------------------------------------- 1 | # Data 2 | -------------------------------------------------------------------------------- /payment/private/data/migrate.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/payment/private/biz" 6 | kitgorm "github.com/go-saas/kit/pkg/gorm" 7 | "github.com/go-saas/saas/seed" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type Migrate struct { 12 | data *Data 13 | } 14 | 15 | func NewMigrate(data *Data) *Migrate { 16 | return &Migrate{ 17 | data: data, 18 | } 19 | } 20 | func (m *Migrate) Seed(ctx context.Context, sCtx *seed.Context) error { 21 | //make sure database exists 22 | ctx = kitgorm.NewDbGuardianContext(ctx) 23 | db := GetDb(ctx, m.data.DbProvider) 24 | return migrateDb(db) 25 | } 26 | 27 | func migrateDb(db *gorm.DB) error { 28 | return db.AutoMigrate(&biz.Subscription{}, &biz.SubscriptionItem{}) 29 | } 30 | -------------------------------------------------------------------------------- /payment/private/service/README.md: -------------------------------------------------------------------------------- 1 | # Service 2 | -------------------------------------------------------------------------------- /payment/private/service/checkout_stripe.go: -------------------------------------------------------------------------------- 1 | package service 2 | -------------------------------------------------------------------------------- /payment/private/service/subscription_internal.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | pb "github.com/go-saas/kit/payment/api/subscription/v1" 6 | sapi "github.com/go-saas/kit/pkg/api" 7 | "github.com/go-saas/saas/data" 8 | ) 9 | 10 | func (s *SubscriptionService) GetInternalSubscription(ctx context.Context, req *pb.GetInternalSubscriptionRequest) (*pb.Subscription, error) { 11 | if err := sapi.ErrIfUntrusted(ctx, s.trusted); err != nil { 12 | return nil, err 13 | } 14 | // disable tenant filter 15 | ctx = data.NewMultiTenancyDataFilter(ctx, false) 16 | g, err := s.subsRepo.Get(ctx, req.GetId()) 17 | if err != nil { 18 | return nil, err 19 | } 20 | if g == nil { 21 | return nil, pb.ErrorSubscriptionNotFoundLocalized(ctx, nil, nil) 22 | } 23 | ret := &pb.Subscription{} 24 | mapBizSubscription2Pb(g, ret) 25 | return ret, nil 26 | } 27 | -------------------------------------------------------------------------------- /payment/private/service/subscription_stripe.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/go-saas/kit/payment/private/biz" 5 | "github.com/stripe/stripe-go/v76" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | func MapStripeSubscription2Biz(a *stripe.Subscription, b *biz.Subscription) { 11 | b.Status = biz.SubscriptionStatus(a.Status) 12 | b.CancelAtPeriodEnd = a.CancelAtPeriodEnd 13 | b.CurrencyCode = strings.ToUpper(string(a.Currency)) 14 | startTime := time.Unix(a.CurrentPeriodStart, 0) 15 | b.CurrentPeriodStart = &startTime 16 | endTime := time.Unix(a.CurrentPeriodEnd, 0) 17 | b.CurrentPeriodEnd = &endTime 18 | 19 | } 20 | -------------------------------------------------------------------------------- /pkg/apisix/apisix.go: -------------------------------------------------------------------------------- 1 | package apisix 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/go-kratos/kratos/v2/log" 6 | "google.golang.org/protobuf/encoding/protojson" 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | var ( 11 | modules []*Module 12 | ) 13 | 14 | func LoadFromYaml(data []byte) { 15 | m := make(map[string]interface{}) 16 | 17 | err := yaml.Unmarshal(data, &m) 18 | if err != nil { 19 | log.Fatalf("error: %v", err) 20 | } 21 | b, err := json.Marshal(m) 22 | if err != nil { 23 | log.Fatalf("error: %v", err) 24 | } 25 | 26 | opt := protojson.UnmarshalOptions{} 27 | module := &Module{} 28 | err = opt.Unmarshal(b, module) 29 | if err != nil { 30 | log.Fatalf("error: %v", err) 31 | } 32 | modules = append(modules, module) 33 | } 34 | 35 | func WalkModules(f func(module *Module) error) error { 36 | for _, module := range modules { 37 | if err := f(module); err != nil { 38 | return err 39 | } 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/apisix/apisix.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package apisix; 4 | 5 | import "google/protobuf/struct.proto"; 6 | 7 | option go_package = "github.com/go-saas/kit/pkg/apisix;apisix"; 8 | 9 | message Node{ 10 | string host=1; 11 | uint64 port=2; 12 | int64 weight=3; 13 | } 14 | 15 | message Upstream{ 16 | repeated Node nodes=1; 17 | string type=2; 18 | string scheme=3; 19 | string name=4; 20 | } 21 | 22 | message Config{ 23 | string endpoint=1; 24 | string api_key=2; 25 | repeated Module modules=3; 26 | } 27 | 28 | message Module{ 29 | // https://apisix.apache.org/docs/apisix/admin-api/#route 30 | map routes=10; 31 | // https://apisix.apache.org/docs/apisix/admin-api/#global-rule 32 | map global_rules=11; 33 | // https://apisix.apache.org/docs/apisix/admin-api/#upstream 34 | map upstreams=12; 35 | // https://apisix.apache.org/docs/apisix/admin-api/#stream-route 36 | map stream_routes=13; 37 | } -------------------------------------------------------------------------------- /pkg/authn/context.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import "context" 4 | 5 | type userKey struct{} 6 | 7 | func NewUserContext(ctx context.Context, user UserInfo) context.Context { 8 | return context.WithValue(ctx, userKey{}, user) 9 | } 10 | 11 | func FromUserContext(ctx context.Context) (user UserInfo, ok bool) { 12 | user, ok = ctx.Value(userKey{}).(UserInfo) 13 | if ok { 14 | return user, ok 15 | } 16 | return NewUserInfo(""), ok 17 | } 18 | 19 | type clientKey struct { 20 | } 21 | 22 | func NewClientContext(ctx context.Context, clientId string) context.Context { 23 | return context.WithValue(ctx, clientKey{}, clientId) 24 | } 25 | 26 | func FromClientContext(ctx context.Context) (clientId string, ok bool) { 27 | clientId, ok = ctx.Value(clientKey{}).(string) 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /pkg/authn/helper.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | import ( 4 | "context" 5 | "github.com/go-kratos/kratos/v2/errors" 6 | ) 7 | 8 | func ErrIfUnauthenticated(ctx context.Context) (UserInfo, error) { 9 | user, ok := FromUserContext(ctx) 10 | if !ok || user.GetId() == "" { 11 | return user, errors.Unauthorized("", "") 12 | } 13 | return user, nil 14 | } 15 | -------------------------------------------------------------------------------- /pkg/authn/jwt/context.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import "context" 4 | 5 | type jwtKey struct{} 6 | type claimKey struct{} 7 | 8 | func NewClaimsContext(ctx context.Context, claims *Claims) context.Context { 9 | return context.WithValue(ctx, claimKey{}, claims) 10 | } 11 | 12 | func FromClaimsContext(ctx context.Context) (claims *Claims, ok bool) { 13 | claims, ok = ctx.Value(claimKey{}).(*Claims) 14 | return 15 | } 16 | 17 | func NewJWTContext(ctx context.Context, jwt string) context.Context { 18 | return context.WithValue(ctx, jwtKey{}, jwt) 19 | } 20 | 21 | func FromJWTContext(ctx context.Context) (jwt string, ok bool) { 22 | jwt, ok = ctx.Value(jwtKey{}).(string) 23 | return 24 | } 25 | -------------------------------------------------------------------------------- /pkg/authn/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | kitdi "github.com/go-saas/kit/pkg/di" 5 | ) 6 | 7 | var ProviderSet = kitdi.NewSet(NewTokenizer, NewTokenizerConfig) 8 | 9 | const ( 10 | AuthorizationHeader = "Authorization" 11 | BearerTokenType = "Bearer" 12 | AuthorizationQuery = "access_token" 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/authn/session/context.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import "context" 4 | 5 | type clientStateKey struct { 6 | } 7 | 8 | type clientStateWriterKey struct { 9 | } 10 | 11 | func NewClientStateContext(ctx context.Context, state ClientState) context.Context { 12 | return context.WithValue(ctx, clientStateKey{}, state) 13 | } 14 | 15 | func FromClientStateContext(ctx context.Context) (state ClientState, ok bool) { 16 | state, ok = ctx.Value(clientStateKey{}).(ClientState) 17 | return 18 | } 19 | 20 | func NewClientStateWriterContext(ctx context.Context, sw ClientStateWriter) context.Context { 21 | return context.WithValue(ctx, clientStateWriterKey{}, sw) 22 | } 23 | 24 | func FromClientStateWriterContext(ctx context.Context) (sw ClientStateWriter, ok bool) { 25 | sw, ok = ctx.Value(clientStateWriterKey{}).(ClientStateWriter) 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /pkg/authn/user_info.go: -------------------------------------------------------------------------------- 1 | package authn 2 | 3 | type UserInfo interface { 4 | GetId() string 5 | } 6 | 7 | type DefaultUserInfo struct { 8 | id string 9 | } 10 | 11 | var _ UserInfo = (*DefaultUserInfo)(nil) 12 | 13 | func NewUserInfo(id string) *DefaultUserInfo { 14 | return &DefaultUserInfo{id: id} 15 | } 16 | 17 | func (d *DefaultUserInfo) GetId() string { 18 | return d.id 19 | } 20 | -------------------------------------------------------------------------------- /pkg/authz/authz/action.go: -------------------------------------------------------------------------------- 1 | package authz 2 | 3 | type ActionStr string 4 | 5 | func (a ActionStr) GetIdentity() string { 6 | return string(a) 7 | } 8 | 9 | const ( 10 | AnyAction ActionStr = "*" 11 | 12 | CreateAction ActionStr = "create" 13 | UpdateAction ActionStr = "update" 14 | DeleteAction ActionStr = "delete" 15 | 16 | ReadAction ActionStr = "read" 17 | WriteAction ActionStr = "write" 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/authz/authz/checker.go: -------------------------------------------------------------------------------- 1 | package authz 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type PermissionChecker interface { 8 | IsGrantTenant(ctx context.Context, requirement RequirementList, tenantID string, subjects ...Subject) ([]Effect, error) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/authz/authz/context.go: -------------------------------------------------------------------------------- 1 | package authz 2 | 3 | import "context" 4 | 5 | type alwaysKey struct{} 6 | 7 | //NewAlwaysAuthorizationContext create a context for always pass or forbidden authorization check. useful for testing 8 | func NewAlwaysAuthorizationContext(ctx context.Context, allow ...bool) context.Context { 9 | v := true 10 | if len(allow) > 0 { 11 | v = allow[0] 12 | } 13 | return context.WithValue(ctx, alwaysKey{}, v) 14 | } 15 | 16 | func FromAlwaysAuthorizationContext(ctx context.Context) (allow bool, ok bool) { 17 | allow, ok = ctx.Value(alwaysKey{}).(bool) 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /pkg/authz/authz/def.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package authz; 4 | 5 | import "google/protobuf/struct.proto"; 6 | option go_package = "github.com/go-saas/kit/pkg/authz/authz;authz"; 7 | 8 | enum PermissionAllowSide{ 9 | BOTH=0; 10 | HOST_ONLY=1; 11 | TENANT_ONLY=2; 12 | } 13 | 14 | // PermissionDefGroup group multiple permission definition 15 | message PermissionDefGroup{ 16 | string name=1; 17 | PermissionAllowSide side=2; 18 | repeated PermissionDef def=3; 19 | google.protobuf.Struct extra=4; 20 | bool internal=5; 21 | int32 priority=6; 22 | } 23 | 24 | 25 | message PermissionDef{ 26 | // name user friendly name 27 | string name=1; 28 | PermissionAllowSide side=2; 29 | string namespace=3; 30 | string action=4; 31 | google.protobuf.Struct extra=5; 32 | // internal will not be displayed by ui 33 | bool internal=6; 34 | int32 priority=7; 35 | } 36 | 37 | message PermissionConf{ 38 | repeated PermissionDefGroup groups=1; 39 | } -------------------------------------------------------------------------------- /pkg/authz/authz/resource.go: -------------------------------------------------------------------------------- 1 | package authz 2 | 3 | import "fmt" 4 | 5 | const ( 6 | AnyNamespace = "*" 7 | AnyResource = "*" 8 | AnyTenant = "*" 9 | ) 10 | 11 | type EntityResource struct { 12 | Namespace string 13 | Id string 14 | } 15 | 16 | var _ Resource = (*EntityResource)(nil) 17 | 18 | func NewEntityResource(namespace string, id string) *EntityResource { 19 | return &EntityResource{namespace, id} 20 | } 21 | 22 | func (r *EntityResource) GetNamespace() string { 23 | return r.Namespace 24 | } 25 | 26 | func (r *EntityResource) GetIdentity() string { 27 | return r.Id 28 | } 29 | 30 | func (r *EntityResource) String() string { 31 | return fmt.Sprintf("%s/%s", r.Namespace, r.Id) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/authz/authz/result.go: -------------------------------------------------------------------------------- 1 | package authz 2 | 3 | type Result struct { 4 | Allowed bool 5 | Requirements []*Requirement 6 | } 7 | 8 | func NewAllowAuthorizationResult() *Result { 9 | return &Result{Allowed: true} 10 | } 11 | 12 | func NewDisallowAuthorizationResult(requirements ...*Requirement) *Result { 13 | return &Result{Allowed: false, Requirements: requirements} 14 | } 15 | -------------------------------------------------------------------------------- /pkg/authz/casbin/model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, namespace, obj, act, ten 3 | 4 | [policy_definition] 5 | p = sub, namespace, obj, act, ten, eft 6 | 7 | [policy_effect] 8 | e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) 9 | 10 | [matchers] 11 | m = keyMatch(r.sub, p.sub) && keyMatch(r.namespace, p.namespace) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act) && keyMatch(r.ten, p.ten) -------------------------------------------------------------------------------- /pkg/authz/readme.md: -------------------------------------------------------------------------------- 1 | # Authorization -------------------------------------------------------------------------------- /pkg/blob/blob.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package blob; 4 | 5 | option go_package = "github.com/go-saas/kit/pkg/blob;blob"; 6 | import "google/protobuf/struct.proto"; 7 | import "google/protobuf/wrappers.proto"; 8 | import "validate/validate.proto"; 9 | 10 | message Config{ 11 | string provider =1; 12 | 13 | string mount_path =2 [(validate.rules).string.min_len=1]; 14 | 15 | string public_url=5; 16 | string internal_url=6; 17 | 18 | ProviderS3 s3=100; 19 | ProviderOs os=101; 20 | } 21 | 22 | message ProviderS3{ 23 | string region=1; 24 | string key=2; 25 | string secret=3; 26 | string bucket=4; 27 | } 28 | 29 | message ProviderOs{ 30 | optional google.protobuf.StringValue dir=1; 31 | } 32 | 33 | message BlobFile{ 34 | string id=1; 35 | string name=2; 36 | string mime=3; 37 | int64 size=4; 38 | string url =5; 39 | google.protobuf.Struct metadata=6; 40 | } -------------------------------------------------------------------------------- /pkg/blob/memory/memory.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "github.com/go-saas/kit/pkg/blob" 5 | "github.com/goxiaoy/vfs" 6 | "github.com/spf13/afero" 7 | "net/url" 8 | ) 9 | 10 | func init() { 11 | blob.Register("memory", func(cfg *blob.Config) (vfs.Blob, error) { 12 | // Initialize the file system 13 | mm := afero.NewMemMapFs() 14 | public, err := url.Parse(cfg.PublicUrl) 15 | if err != nil { 16 | return nil, err 17 | } 18 | internal, err := url.Parse(cfg.InternalUrl) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return vfs.NewOptLinker(mm, *public, *internal, nil), nil 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/blob/os/os.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "github.com/go-saas/kit/pkg/blob" 5 | "github.com/goxiaoy/vfs" 6 | "github.com/spf13/afero" 7 | "net/url" 8 | ) 9 | 10 | func init() { 11 | blob.Register("os", func(cfg *blob.Config) (vfs.Blob, error) { 12 | // Initialize the file system 13 | appfs := afero.NewOsFs() 14 | if cfg.Os != nil && cfg.Os.Dir != nil { 15 | appfs = afero.NewBasePathFs(appfs, cfg.Os.Dir.Value) 16 | } 17 | public, err := url.Parse(cfg.PublicUrl) 18 | if err != nil { 19 | return nil, err 20 | } 21 | internal, err := url.Parse(cfg.InternalUrl) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return vfs.NewOptLinker(appfs, *public, *internal, nil), nil 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/blob/s3/s3.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/credentials" 6 | "github.com/aws/aws-sdk-go/aws/session" 7 | "github.com/go-saas/kit/pkg/blob" 8 | "github.com/goxiaoy/vfs" 9 | s32 "github.com/goxiaoy/vfs/s3" 10 | "net/url" 11 | "time" 12 | ) 13 | 14 | func init() { 15 | blob.Register("s3", func(cfg *blob.Config) (vfs.Blob, error) { 16 | // You create a session 17 | sess, _ := session.NewSession(&aws.Config{ 18 | Region: aws.String(cfg.S3.Region), 19 | Credentials: credentials.NewStaticCredentials(cfg.S3.Key, cfg.S3.Secret, ""), 20 | }) 21 | 22 | public, err := url.Parse(cfg.PublicUrl) 23 | if err != nil { 24 | return nil, err 25 | } 26 | internal, err := url.Parse(cfg.InternalUrl) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return s32.NewBlob(sess, cfg.S3.Bucket, *public, *internal, time.Hour*24*15), nil 32 | 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/dal/readme.md: -------------------------------------------------------------------------------- 1 | # Data Access Layer 2 | -------------------------------------------------------------------------------- /pkg/data/data.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package data; 4 | 5 | import "google/protobuf/struct.proto"; 6 | import "query/operation.proto"; 7 | 8 | option go_package = "github.com/go-saas/kit/pkg/data;data"; 9 | 10 | message DynamicValue{ 11 | oneof value{ 12 | int32 int_value=2; 13 | int64 long_value=3; 14 | float float_value=4; 15 | double double_value=5; 16 | string string_value=6; 17 | bool bool_value=7; 18 | google.protobuf.NullValue null_value=8; 19 | google.protobuf.Struct json_value=9; 20 | } 21 | } 22 | 23 | message DynamicValueFilter{ 24 | oneof filter{ 25 | query.operation.Int32FilterOperators int_value=2; 26 | query.operation.Int64FilterOperators long_value=3; 27 | query.operation.FloatFilterOperators float_value=4; 28 | query.operation.DoubleFilterOperators double_value=5; 29 | query.operation.StringFilterOperation string_value=6; 30 | query.operation.BooleanFilterOperators bool_value=7; 31 | query.operation.NullFilterOperators null_value=8; 32 | } 33 | } -------------------------------------------------------------------------------- /pkg/data/readme.md: -------------------------------------------------------------------------------- 1 | Shared structure and basic repo -------------------------------------------------------------------------------- /pkg/email/conf.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | func (x *Config) Normalize() { 4 | if len(x.Provider) == 0 { 5 | if x.Smtp != nil { 6 | x.Provider = "smtp" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pkg/email/email.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/goava/di" 7 | mail "github.com/wneessen/go-mail" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | type Client interface { 12 | Send(context.Context, ...*mail.Msg) error 13 | } 14 | 15 | func NewClient(config *Config, container *di.Container) (Client, error) { 16 | defConf := &Config{Provider: "log"} 17 | if config != nil { 18 | config.Normalize() 19 | proto.Merge(defConf, config) 20 | } 21 | 22 | _typeMux.RLock() 23 | defer _typeMux.RUnlock() 24 | 25 | f, ok := _typeRegister[defConf.Provider] 26 | if !ok { 27 | return nil, fmt.Errorf("email provider %s not registered", defConf.Provider) 28 | } 29 | return f(config, container) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/email/email.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package email; 4 | 5 | import "google/protobuf/duration.proto"; 6 | import "google/protobuf/wrappers.proto"; 7 | 8 | option go_package = "github.com/go-saas/kit/pkg/email;email"; 9 | 10 | message Config{ 11 | string provider=1; 12 | string from = 2; 13 | 14 | message SMTP{ 15 | string host=1; 16 | optional int32 port=2; 17 | string username=3; 18 | string password=4; 19 | 20 | google.protobuf.Duration timeout=7; 21 | bool tls_skip_verify=9; 22 | } 23 | SMTP smtp=100; 24 | } -------------------------------------------------------------------------------- /pkg/email/factory.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "github.com/goava/di" 5 | "sync" 6 | ) 7 | 8 | type ProviderFunc func(c *Config, container *di.Container) (Client, error) 9 | 10 | var ( 11 | _typeMux sync.RWMutex 12 | _typeRegister = map[string]ProviderFunc{} 13 | ) 14 | 15 | func RegisterProvider(kind string, f ProviderFunc) { 16 | if len(kind) == 0 { 17 | panic("kind is required") 18 | } 19 | _typeMux.Lock() 20 | defer _typeMux.Unlock() 21 | _typeRegister[kind] = f 22 | } 23 | -------------------------------------------------------------------------------- /pkg/email/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "github.com/go-kratos/kratos/v2/log" 6 | "github.com/go-saas/kit/pkg/email" 7 | "github.com/goava/di" 8 | "github.com/samber/lo" 9 | mail "github.com/wneessen/go-mail" 10 | ) 11 | 12 | func init() { 13 | email.RegisterProvider("log", func(c *email.Config, container *di.Container) (email.Client, error) { 14 | var logger log.Logger 15 | err := container.Resolve(&logger) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return &client{l: log.NewHelper(logger)}, nil 20 | }) 21 | } 22 | 23 | type client struct { 24 | l *log.Helper 25 | } 26 | 27 | func (c *client) Send(ctx context.Context, email ...*mail.Msg) error { 28 | for _, m := range email { 29 | parts := lo.Map(m.GetParts(), func(t *mail.Part, _ int) string { 30 | content, _ := t.GetContent() 31 | return string(content) 32 | }) 33 | c.l.Infow(log.DefaultMessageKey, "send email", "to", m.GetToString(), "parts", parts) 34 | } 35 | return nil 36 | } 37 | 38 | var _ email.Client = (*client)(nil) 39 | -------------------------------------------------------------------------------- /pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "github.com/go-kratos/kratos/v2/errors" 4 | 5 | func UnRecoverableError(err error) bool { 6 | fr := errors.FromError(err) 7 | if fr.Code < 500 { 8 | return false 9 | } 10 | return true 11 | } 12 | -------------------------------------------------------------------------------- /pkg/flag/flag.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | type ArrayFlags []string 4 | 5 | func (i *ArrayFlags) String() string { 6 | return "my string representation" 7 | } 8 | 9 | func (i *ArrayFlags) Set(value string) error { 10 | *i = append(*i, value) 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /pkg/gorm/base.go: -------------------------------------------------------------------------------- 1 | package gorm 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type UIDBase struct { 9 | ID uuid.UUID `gorm:"type:char(36)" json:"id"` 10 | } 11 | 12 | func (u *UIDBase) BeforeCreate(tx *gorm.DB) error { 13 | if u.ID == uuid.Nil { 14 | u.ID = uuid.New() 15 | } 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /pkg/gorm/context.go: -------------------------------------------------------------------------------- 1 | package gorm 2 | 3 | import ( 4 | "context" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type ( 9 | contextDbKey string 10 | ) 11 | 12 | func NewContext(ctx context.Context, key string, db *gorm.DB) context.Context { 13 | return context.WithValue(ctx, contextDbKey(key), db) 14 | } 15 | 16 | func fromContext(ctx context.Context, key contextDbKey) (*gorm.DB, bool) { 17 | v, ok := ctx.Value(key).(*gorm.DB) 18 | return v, ok 19 | } 20 | -------------------------------------------------------------------------------- /pkg/gorm/scopes.go: -------------------------------------------------------------------------------- 1 | package gorm 2 | 3 | import "gorm.io/gorm" 4 | 5 | // WhereUserId append 'user_id' field filter 6 | func WhereUserId(id string) func(db *gorm.DB) *gorm.DB { 7 | return func(db *gorm.DB) *gorm.DB { 8 | return db.Where("user_id = ?", id) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pkg/idgen/idgen.go: -------------------------------------------------------------------------------- 1 | package idgen 2 | 3 | import "context" 4 | 5 | type Generator interface { 6 | Gen(ctx context.Context) (string, error) 7 | } 8 | 9 | type GeneratorFunc func(ctx context.Context) (string, error) 10 | 11 | func (g GeneratorFunc) Gen(ctx context.Context) (string, error) { 12 | return g(ctx) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/idgen/ksuid.go: -------------------------------------------------------------------------------- 1 | package idgen 2 | 3 | import ( 4 | "context" 5 | "github.com/segmentio/ksuid" 6 | ) 7 | 8 | type Ksuid struct { 9 | } 10 | 11 | func (u *Ksuid) Gen(ctx context.Context) (string, error) { 12 | uid := ksuid.New() 13 | return uid.String(), nil 14 | } 15 | -------------------------------------------------------------------------------- /pkg/idgen/shortuuid.go: -------------------------------------------------------------------------------- 1 | package idgen 2 | 3 | import ( 4 | "context" 5 | "github.com/lithammer/shortuuid/v3" 6 | ) 7 | 8 | type ShortUuid struct { 9 | } 10 | 11 | func (u *ShortUuid) Gen(ctx context.Context) (string, error) { 12 | return shortuuid.New(), nil 13 | } 14 | -------------------------------------------------------------------------------- /pkg/idgen/uuid.go: -------------------------------------------------------------------------------- 1 | package idgen 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type Uuid struct { 9 | } 10 | 11 | func (u *Uuid) Gen(ctx context.Context) (string, error) { 12 | uid, err := uuid.NewRandom() 13 | if err != nil { 14 | return "", err 15 | } 16 | return uid.String(), nil 17 | } -------------------------------------------------------------------------------- /pkg/idp/idp.go: -------------------------------------------------------------------------------- 1 | package idp 2 | -------------------------------------------------------------------------------- /pkg/idp/idp_errors.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: idp/idp_errors.proto 3 | 4 | package idp 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | 19 | "google.golang.org/protobuf/types/known/anypb" 20 | ) 21 | 22 | // ensure the imports are used 23 | var ( 24 | _ = bytes.MinRead 25 | _ = errors.New("") 26 | _ = fmt.Print 27 | _ = utf8.UTFMax 28 | _ = (*regexp.Regexp)(nil) 29 | _ = (*strings.Reader)(nil) 30 | _ = net.IPv4len 31 | _ = time.Duration(0) 32 | _ = (*url.URL)(nil) 33 | _ = (*mail.Address)(nil) 34 | _ = anypb.Any{} 35 | _ = sort.Sort 36 | ) 37 | -------------------------------------------------------------------------------- /pkg/idp/idp_errors.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package idp; 4 | 5 | import "errors/errors.proto"; 6 | 7 | option go_package = "github.com/go-saas/kit/pkg/idp;idp"; 8 | 9 | enum ErrorReason { 10 | 11 | option (errors.default_code) = 500; 12 | 13 | WECHAT_CONFIG_NOT_FOUND = 0 [(errors.code) = 400]; 14 | } -------------------------------------------------------------------------------- /pkg/idp/stripe.go: -------------------------------------------------------------------------------- 1 | package idp 2 | 3 | import "github.com/go-saas/kit/pkg/stripe" 4 | 5 | const ( 6 | StripeLoginProvider = stripe.ProviderName 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/job/ui.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "github.com/hibiken/asynq" 5 | "github.com/hibiken/asynqmon" 6 | "net/http" 7 | ) 8 | 9 | func NewUi(root string, opt asynq.RedisConnOpt) http.Handler { 10 | h := asynqmon.New(asynqmon.Options{ 11 | RootPath: root, // RootPath specifies the root for asynqmon app 12 | RedisConnOpt: opt, 13 | }) 14 | return h 15 | } 16 | -------------------------------------------------------------------------------- /pkg/mapstructure/mapstructure.go: -------------------------------------------------------------------------------- 1 | package mapstructure 2 | 3 | import ( 4 | data2 "github.com/go-saas/kit/pkg/data" 5 | "github.com/google/uuid" 6 | "github.com/mitchellh/mapstructure" 7 | "reflect" 8 | ) 9 | 10 | func StringToUUIDHookFunc() mapstructure.DecodeHookFunc { 11 | return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { 12 | if f.Kind() != reflect.String { 13 | return data, nil 14 | } 15 | if t != reflect.TypeOf(uuid.UUID{}) { 16 | return data, nil 17 | } 18 | 19 | return uuid.Parse(data.(string)) 20 | } 21 | } 22 | 23 | func JsonToJsonMapHookFunc() mapstructure.DecodeHookFunc { 24 | return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { 25 | 26 | if v, ok := data.(map[string]interface{}); ok { 27 | return data2.JSONMap(v), nil 28 | } 29 | return data, nil 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/price/price.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package price; 4 | option go_package = "github.com/go-saas/kit/pkg/price;price"; 5 | 6 | message PricePb{ 7 | int64 amount=1; 8 | string currency_code=2; 9 | int32 digits=3; 10 | string text=4; 11 | string amount_decimal=5; 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /pkg/price/price_test.go: -------------------------------------------------------------------------------- 1 | package price 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/pkg/localize" 6 | "github.com/stretchr/testify/assert" 7 | "golang.org/x/text/language" 8 | "testing" 9 | ) 10 | 11 | func TestPrice(t *testing.T) { 12 | p, err := NewPriceFromInt64(1000, "CNY") 13 | assert.NoError(t, err) 14 | ctx := localize.NewLanguageTagsContext(context.TODO(), []language.Tag{language.Chinese}) 15 | pricePb := p.ToPricePb(ctx) 16 | assert.Equal(t, "¥10.00", pricePb.Text) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/query/operation.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/samber/lo" 5 | "google.golang.org/protobuf/types/known/wrapperspb" 6 | ) 7 | 8 | func FromString(s []string) []*wrapperspb.StringValue { 9 | return lo.Map(s, func(t string, _ int) *wrapperspb.StringValue { 10 | return &wrapperspb.StringValue{Value: t} 11 | }) 12 | } 13 | 14 | func FromFloat64(s []float64) []*wrapperspb.DoubleValue { 15 | return lo.Map(s, func(t float64, _ int) *wrapperspb.DoubleValue { 16 | return &wrapperspb.DoubleValue{Value: t} 17 | }) 18 | } 19 | 20 | func FromFlot32(s []float32) []*wrapperspb.FloatValue { 21 | return lo.Map(s, func(t float32, _ int) *wrapperspb.FloatValue { 22 | return &wrapperspb.FloatValue{Value: t} 23 | }) 24 | } 25 | func FromInt32(s []int32) []*wrapperspb.Int32Value { 26 | return lo.Map(s, func(t int32, _ int) *wrapperspb.Int32Value { 27 | return &wrapperspb.Int32Value{Value: t} 28 | }) 29 | } 30 | func FromInt64(s []int64) []*wrapperspb.Int64Value { 31 | return lo.Map(s, func(t int64, _ int) *wrapperspb.Int64Value { 32 | return &wrapperspb.Int64Value{Value: t} 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/redis/redis.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package redis; 4 | 5 | import "google/protobuf/duration.proto"; 6 | import "google/protobuf/wrappers.proto"; 7 | 8 | option go_package = "github.com/go-saas/kit/pkg/redis;redis"; 9 | 10 | message Config { 11 | repeated string addrs = 2; 12 | optional google.protobuf.Duration read_timeout = 3; 13 | optional google.protobuf.Duration write_timeout = 4; 14 | optional google.protobuf.StringValue username = 5; 15 | optional google.protobuf.StringValue password = 6; 16 | optional google.protobuf.Int32Value db = 7; 17 | optional google.protobuf.Int32Value max_retries = 8; 18 | optional google.protobuf.Duration min_retry_backoff = 9; 19 | optional google.protobuf.Duration max_retry_backoff = 10; 20 | optional google.protobuf.Duration dial_timeout = 11; 21 | optional string master_name=12; 22 | } 23 | -------------------------------------------------------------------------------- /pkg/registry/etcd/README.md: -------------------------------------------------------------------------------- 1 | #etcd 2 | 3 | Adapted from https://github.com/go-kratos/kratos/tree/main/contrib/registry/etcd -------------------------------------------------------------------------------- /pkg/registry/etcd/service.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/go-kratos/kratos/v2/registry" 7 | ) 8 | 9 | func marshal(si *registry.ServiceInstance) (string, error) { 10 | data, err := json.Marshal(si) 11 | if err != nil { 12 | return "", err 13 | } 14 | return string(data), nil 15 | } 16 | 17 | func unmarshal(data []byte) (si *registry.ServiceInstance, err error) { 18 | err = json.Unmarshal(data, &si) 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /pkg/registry/registry.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package registry; 4 | 5 | option go_package = "github.com/go-saas/kit/pkg/registry;registry"; 6 | 7 | message Config{ 8 | string type=1; 9 | string endpoint=2; 10 | } -------------------------------------------------------------------------------- /pkg/server/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/go-saas/kit/pkg/conf" 5 | "google.golang.org/protobuf/proto" 6 | "google.golang.org/protobuf/types/known/durationpb" 7 | "time" 8 | ) 9 | 10 | const ( 11 | DefaultSrvName = "default" 12 | ) 13 | 14 | var ( 15 | DefaultServerConfig = &conf.Server{ 16 | Http: &conf.Server_HTTP{ 17 | Addr: ":9080", 18 | Timeout: durationpb.New(5 * time.Second), 19 | }, 20 | Grpc: &conf.Server_GRPC{ 21 | Addr: ":9081", 22 | Timeout: durationpb.New(5 * time.Second), 23 | }, 24 | } 25 | ) 26 | 27 | func GetConf(services *conf.Services, name string) *conf.Server { 28 | //default config 29 | server := proto.Clone(DefaultServerConfig).(*conf.Server) 30 | if def, ok := services.Servers[DefaultSrvName]; ok { 31 | //merge default config 32 | proto.Merge(server, def) 33 | } 34 | if s, ok := services.Servers[name]; ok { 35 | //merge service config 36 | proto.Merge(server, s) 37 | } 38 | return server 39 | } 40 | -------------------------------------------------------------------------------- /pkg/server/endpoint/endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | // NewEndpoint new an Endpoint URL. 8 | func NewEndpoint(scheme, host string) *url.URL { 9 | return &url.URL{Scheme: scheme, Host: host} 10 | } 11 | 12 | // ParseEndpoint parses an Endpoint URL. 13 | func ParseEndpoint(endpoints []string, scheme string) (string, error) { 14 | for _, e := range endpoints { 15 | u, err := url.Parse(e) 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | if u.Scheme == scheme { 21 | return u.Host, nil 22 | } 23 | } 24 | return "", nil 25 | } 26 | 27 | // Scheme is the scheme of endpoint url. 28 | // examples: scheme="http",isSecure=true get "https" 29 | func Scheme(scheme string, isSecure bool) string { 30 | if isSecure { 31 | return scheme + "s" 32 | } 33 | return scheme 34 | } 35 | -------------------------------------------------------------------------------- /pkg/server/endpoint/readme.md: -------------------------------------------------------------------------------- 1 | https://github.com/go-kratos/kratos/tree/main/internal/endpoint -------------------------------------------------------------------------------- /pkg/server/errors.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/v2/middleware" 5 | "github.com/go-kratos/kratos/v2/middleware/recovery" 6 | ) 7 | 8 | // Recovery wrap kratos recovery with handler 9 | func Recovery() middleware.Middleware { 10 | return recovery.Recovery() 11 | } 12 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/v2/registry" 5 | "github.com/go-saas/kit/pkg/conf" 6 | kitdi "github.com/go-saas/kit/pkg/di" 7 | kregistry "github.com/go-saas/kit/pkg/registry" 8 | "github.com/go-saas/kit/pkg/server/http" 9 | "github.com/goava/di" 10 | ) 11 | 12 | var DefaultProviderSet = kitdi.NewSet( 13 | kitdi.Value(http.ReqDecode), 14 | kitdi.Value(http.ResEncoder), 15 | kitdi.Value(http.ErrEncoder), 16 | NewRegistrar, 17 | NewWebMultiTenancyOption, 18 | ) 19 | 20 | // Name server name. 21 | type Name string 22 | 23 | func NewRegistrar(services *conf.Services, container *di.Container) (registry.Registrar, error) { 24 | err := container.Provide(func() *kregistry.Config { return services.Registry }) 25 | if err != nil { 26 | return nil, err 27 | } 28 | r, _, err := kregistry.NewRegister(services.Registry, container) 29 | return r, err 30 | } 31 | -------------------------------------------------------------------------------- /pkg/sms/sms.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "context" 5 | "github.com/go-kratos/kratos/v2/log" 6 | ) 7 | 8 | // Sender sends SMS messages to a phone number 9 | type Sender interface { 10 | Send(ctx context.Context, number, text string) error 11 | } 12 | 13 | type LogSender struct { 14 | log *log.Helper 15 | } 16 | 17 | func NewSMSLogSender(l log.Logger) *LogSender { 18 | return &LogSender{log: log.NewHelper(l)} 19 | } 20 | 21 | // Send an SMS 22 | func (s *LogSender) Send(_ context.Context, number, text string) error { 23 | s.log.Info("sms sent to:", number, "contents:", text) 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/stripe/stripe.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package stripe; 4 | 5 | option go_package = "github.com/go-saas/kit/pkg/stripe;stripe"; 6 | 7 | message Conf{ 8 | bool is_test=1; 9 | string publish_key=2; 10 | string private_key=3; 11 | string webhook_key=4; 12 | map price_tables=5; 13 | } 14 | 15 | message Invoice{ 16 | string id=1; 17 | 18 | PaymentIntent payment_intent=10; 19 | } 20 | 21 | message PaymentIntent{ 22 | string id=1; 23 | string client_secret=2; 24 | string status=10; 25 | } 26 | 27 | message Subscription{ 28 | string id=1; 29 | Invoice latest_invoice=100; 30 | } 31 | 32 | message EphemeralKey{ 33 | string secret=1; 34 | } -------------------------------------------------------------------------------- /pkg/utils/hash.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "hash/fnv" 4 | 5 | func Hash(s string) uint32 { 6 | h := fnv.New32a() 7 | h.Write([]byte(s)) 8 | return h.Sum32() 9 | } 10 | -------------------------------------------------------------------------------- /pkg/utils/sort.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | //SortBy sort b by order of a, for solving n+1 4 | func SortBy[T comparable, R any](a []T, b []R, f func(r R) T) []R { 5 | ret := make([]R, len(a)) 6 | for i, t := range a { 7 | for _, r := range b { 8 | if f(r) == t { 9 | ret[i] = r 10 | } 11 | } 12 | } 13 | return ret 14 | } 15 | 16 | func SortById[T comparable, R interface{ GetId() T }](a []T, b []R) []R { 17 | return SortBy[T, R](a, b, func(r R) T { return r.GetId() }) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func String(a string) *string { 4 | if len(a) == 0 { 5 | return nil 6 | } 7 | return &a 8 | } 9 | -------------------------------------------------------------------------------- /product/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 AS builder 2 | 3 | COPY . /src 4 | WORKDIR /src 5 | 6 | RUN make -f ./product/Makefile build 7 | 8 | FROM debian:stable-slim 9 | 10 | RUN apt-get update && apt-get install -y --no-install-recommends \ 11 | ca-certificates \ 12 | netbase \ 13 | && rm -rf /var/lib/apt/lists/ \ 14 | && apt-get autoremove -y && apt-get autoclean -y 15 | 16 | COPY --from=builder /src/bin /app 17 | 18 | WORKDIR /app 19 | 20 | EXPOSE 8000 21 | EXPOSE 9000 22 | VOLUME /data/conf 23 | 24 | CMD ["./product", "-conf", "/data/conf"] 25 | -------------------------------------------------------------------------------- /product/Makefile: -------------------------------------------------------------------------------- 1 | GOPATH:=$(shell go env GOPATH) 2 | VERSION=$(shell git describe --tags --always) 3 | 4 | 5 | .PHONY: build 6 | # build 7 | build: 8 | mkdir -p bin/ && go build -buildvcs=false -ldflags "-X main.Version=$(VERSION)" -o ./bin/ ./... 9 | 10 | .PHONY: api 11 | # generate api proto 12 | api: 13 | cd .. && buf generate --path ./buf/product --template ./product/buf.gen.yaml 14 | 15 | .PHONY: all 16 | # generate all 17 | all: 18 | make api; 19 | 20 | # show help 21 | help: 22 | @echo '' 23 | @echo 'Usage:' 24 | @echo ' make [target]' 25 | @echo '' 26 | @echo 'Targets:' 27 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 28 | helpMessage = match(lastLine, /^# (.*)/); \ 29 | if (helpMessage) { \ 30 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 31 | helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \ 32 | printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \ 33 | } \ 34 | } \ 35 | { lastLine = $$0 }' $(MAKEFILE_LIST) 36 | 37 | .DEFAULT_GOAL := help 38 | -------------------------------------------------------------------------------- /product/README.md: -------------------------------------------------------------------------------- 1 | # Product 2 | -------------------------------------------------------------------------------- /product/api/gateway.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/apisix" 6 | ) 7 | 8 | //go:embed gateway.yaml 9 | var gateway []byte 10 | 11 | func init() { 12 | apisix.LoadFromYaml(gateway) 13 | } 14 | -------------------------------------------------------------------------------- /product/api/gateway.yaml: -------------------------------------------------------------------------------- 1 | routes: 2 | product-api: 3 | uris: ["/v1/product","/v1/product/*","/assets/product/*"] 4 | upstream_id: product-http -------------------------------------------------------------------------------- /product/api/permission.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/authz/authz" 6 | ) 7 | 8 | const ( 9 | ResourceProduct = "product.product" 10 | ) 11 | 12 | //go:embed permission.yaml 13 | var permission []byte 14 | 15 | func init() { 16 | authz.LoadFromYaml(permission) 17 | } 18 | -------------------------------------------------------------------------------- /product/api/permission.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: "permission.product.product.group" 3 | def: 4 | - name: "permission.product.product.any" 5 | namespace: "product.product" 6 | action: "*" 7 | internal: true 8 | - name: "permission.product.product.read" 9 | namespace: "product.product" 10 | action: "read" 11 | - name: "permission.product.product.create" 12 | namespace: "product.product" 13 | action: "create" 14 | - name: "permission.product.product.update" 15 | namespace: "product.product" 16 | action: "update" 17 | - name: "permission.product.product.delete" 18 | namespace: "product.product" 19 | action: "delete" 20 | 21 | 22 | -------------------------------------------------------------------------------- /product/api/product/v1/common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package product.api.product.v1; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | import "google/protobuf/struct.proto"; 7 | 8 | import "google/protobuf/field_mask.proto"; 9 | import "validate/validate.proto"; 10 | 11 | option go_package = "github.com/go-saas/kit/product/api/product/v1;v1"; 12 | 13 | message ProductMedia{ 14 | string id =1; 15 | string type=2; 16 | string mime_type=3; 17 | string name=4; 18 | string url=5; 19 | } 20 | 21 | message Badge{ 22 | string id=1; 23 | string code=3; 24 | string label=4; 25 | } 26 | 27 | message Keyword{ 28 | string id=1; 29 | string text=2; 30 | string refer=3; 31 | } 32 | 33 | 34 | message CampaignRule{ 35 | string id=1; 36 | string rule=2; 37 | google.protobuf.Struct extra=3; 38 | } 39 | 40 | message Stock{ 41 | string id=1; 42 | bool in_stock=2; 43 | string level=3 [(validate.rules).string={in:["out","in","low"]}]; 44 | int32 amount=10; 45 | string delivery_code=11; 46 | } 47 | 48 | 49 | message ProductAttribute{ 50 | string title=10; 51 | } -------------------------------------------------------------------------------- /product/api/product/v1/error_reason.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: product/api/product/v1/error_reason.proto 3 | 4 | package v1 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | 19 | "google.golang.org/protobuf/types/known/anypb" 20 | ) 21 | 22 | // ensure the imports are used 23 | var ( 24 | _ = bytes.MinRead 25 | _ = errors.New("") 26 | _ = fmt.Print 27 | _ = utf8.UTFMax 28 | _ = (*regexp.Regexp)(nil) 29 | _ = (*strings.Reader)(nil) 30 | _ = net.IPv4len 31 | _ = time.Duration(0) 32 | _ = (*url.URL)(nil) 33 | _ = (*mail.Address)(nil) 34 | _ = anypb.Any{} 35 | _ = sort.Sort 36 | ) 37 | -------------------------------------------------------------------------------- /product/api/product/v1/error_reason.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package product.api.product.v1; 4 | import "errors/errors.proto"; 5 | 6 | option go_package = "github.com/go-saas/kit/product/api/product/v1;v1"; 7 | 8 | enum ErrorReason { 9 | option (errors.default_code) = 500; 10 | 11 | CONTENT_MISSING = 0 [(errors.code) = 400]; 12 | PRODUCT_MANAGED= 1 [(errors.code) = 403]; 13 | } 14 | -------------------------------------------------------------------------------- /product/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: buf.build/grpc-ecosystem/openapiv2:v2.15.2 4 | out: ./product/private/service/openapi 5 | opt: 6 | - allow_merge=true 7 | - proto3_optional_nullable=true 8 | - merge_file_name=api -------------------------------------------------------------------------------- /product/event/v1/event.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: product/event/v1/event.proto 3 | 4 | package v1 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | 19 | "google.golang.org/protobuf/types/known/anypb" 20 | ) 21 | 22 | // ensure the imports are used 23 | var ( 24 | _ = bytes.MinRead 25 | _ = errors.New("") 26 | _ = fmt.Print 27 | _ = utf8.UTFMax 28 | _ = (*regexp.Regexp)(nil) 29 | _ = (*strings.Reader)(nil) 30 | _ = net.IPv4len 31 | _ = time.Duration(0) 32 | _ = (*url.URL)(nil) 33 | _ = (*mail.Address)(nil) 34 | _ = anypb.Any{} 35 | _ = sort.Sort 36 | ) 37 | -------------------------------------------------------------------------------- /product/event/v1/event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package product.event; 3 | 4 | option go_package = "github.com/go-saas/kit/product/event/v1;v1"; 5 | 6 | -------------------------------------------------------------------------------- /product/i18n/embed/en-US.toml: -------------------------------------------------------------------------------- 1 | [permission.product.product.group] 2 | other = "Product" 3 | [permission.product.product.read] 4 | other = "Read Product" 5 | [permission.product.product.create] 6 | other = "Create Product" 7 | [permission.product.product.update] 8 | other = "Update Product" 9 | [permission.product.product.delete] 10 | other = "Delete Product" 11 | 12 | [product.manager] 13 | other = "Product Management" 14 | [product.product] 15 | other = "Product List" 16 | 17 | [ProductManaged] 18 | other = "Product is managed!" -------------------------------------------------------------------------------- /product/i18n/embed/zh-CN.toml: -------------------------------------------------------------------------------- 1 | [permission.product.product.group] 2 | other = "产品" 3 | [permission.product.product.read] 4 | other = "读取产品" 5 | [permission.product.product.create] 6 | other = "创建产品" 7 | [permission.product.product.update] 8 | other = "更新产品" 9 | [permission.product.product.delete] 10 | other = "删除产品" 11 | 12 | [product.manager] 13 | other = "产品管理" 14 | [product.product] 15 | other = "产品列表" 16 | 17 | [ProductManaged] 18 | other = "产品已被托管" -------------------------------------------------------------------------------- /product/i18n/i18n.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "embed" 5 | "github.com/go-saas/kit/pkg/localize" 6 | ) 7 | 8 | var ( 9 | //go:embed embed 10 | f embed.FS 11 | ) 12 | 13 | func init() { 14 | localize.RegisterFileBundle(localize.FileBundle{Fs: f}) 15 | } 16 | -------------------------------------------------------------------------------- /product/menu/menu.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/sys/menu" 6 | ) 7 | 8 | //go:embed menu.yaml 9 | var menuData []byte 10 | 11 | func init() { 12 | menu.LoadFromYaml(menuData) 13 | } 14 | -------------------------------------------------------------------------------- /product/menu/menu.yaml: -------------------------------------------------------------------------------- 1 | menus: 2 | - name: "product" 3 | path: "/product" 4 | component: "LAYOUT" 5 | redirect: "/product/products" 6 | is_preserved: true 7 | title: "product.manager" 8 | icon: "ShoppingOutlined" 9 | meta: 10 | priority: 200 11 | children: 12 | - name: "product.product" 13 | path: "/product/products" 14 | component: "/Product/Product" 15 | title: "product.product" 16 | requirement: 17 | - namespace: "product.product" 18 | resource: "*" 19 | action: "read" 20 | -------------------------------------------------------------------------------- /product/private/biz/README.md: -------------------------------------------------------------------------------- 1 | # Biz 2 | -------------------------------------------------------------------------------- /product/private/biz/biz.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/go-saas/kit/pkg/dal" 5 | kitdi "github.com/go-saas/kit/pkg/di" 6 | "github.com/go-saas/kit/pkg/stripe" 7 | ) 8 | 9 | // ProviderSet is biz providers. 10 | var ProviderSet = kitdi.NewSet(NewPostSeeder) 11 | 12 | var ( 13 | ProductMediaPath = "product/m" 14 | ) 15 | 16 | type ProductManageProvider string 17 | 18 | const ( 19 | ProductManageProviderInternal ProductManageProvider = "internal" 20 | ProductManageProviderStripe ProductManageProvider = stripe.ProviderName 21 | ) 22 | 23 | const ConnName dal.ConnName = "product" 24 | -------------------------------------------------------------------------------- /product/private/biz/brand.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/go-saas/kit/pkg/gorm" 5 | "github.com/go-saas/kit/pkg/multilingual" 6 | "github.com/samber/lo" 7 | ) 8 | 9 | type Brand struct { 10 | gorm.UIDBase 11 | Code string 12 | Name string 13 | Logo string 14 | Url string 15 | Desc string 16 | 17 | Trans []*BrandTrans 18 | 19 | OwnedTenantId string 20 | } 21 | 22 | type BrandTrans struct { 23 | gorm.UIDBase 24 | multilingual.Embed 25 | 26 | BrandId string 27 | 28 | Name string 29 | Url string 30 | Description string 31 | } 32 | 33 | var _ multilingual.Multilingual = (*Brand)(nil) 34 | 35 | func (b *Brand) GetTranslations() []interface{} { 36 | return lo.Map(b.Trans, func(item *BrandTrans, _ int) interface{} { 37 | return item 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /product/private/biz/category.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/pkg/data" 6 | kitgorm "github.com/go-saas/kit/pkg/gorm" 7 | v1 "github.com/go-saas/kit/product/api/category/v1" 8 | ) 9 | 10 | // ProductCategory represents some Teaser infos for ProductCategory 11 | type ProductCategory struct { 12 | // Key the identifier of the ProductCategory 13 | Key string `gorm:"primaryKey;size:128"` 14 | 15 | kitgorm.AuditedModel 16 | // The Path (root to leaf) for this ProductCategory - separated by "/" 17 | Path string 18 | // Name is the speaking name of the category 19 | Name string 20 | ParentID *string 21 | 22 | // Parent is an optional link to parent teaser 23 | Parent *ProductCategory `gorm:"foreignKey:ParentID;references:key"` 24 | } 25 | 26 | type ProductCategoryRepo interface { 27 | data.Repo[ProductCategory, string, *v1.ListProductCategoryRequest] 28 | FindAllChildren(ctx context.Context, entity *ProductCategory) ([]*ProductCategory, error) 29 | FindByKeys(ctx context.Context, cKeys []string) ([]ProductCategory, error) 30 | } 31 | -------------------------------------------------------------------------------- /product/private/biz/job.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package product.private.biz; 3 | 4 | option go_package = "github.com/go-saas/kit/product/private/biz;biz"; 5 | 6 | message ProductUpdatedJobParam{ 7 | string product_id=1; 8 | string product_version=2; 9 | string tenant_id=3; 10 | bool is_delete=4; 11 | 12 | message SyncLink{ 13 | string provider_name=1; 14 | string provider_id=2; 15 | } 16 | repeated SyncLink sync_links=5; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /product/private/biz/media.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/go-saas/kit/pkg/data" 5 | "github.com/go-saas/kit/pkg/sortable" 6 | ) 7 | 8 | type ProductMedia struct { 9 | ID string `gorm:"primaryKey;size:128"` 10 | 11 | OwnerID string 12 | // OwnerType product/product_sku 13 | OwnerType string 14 | 15 | Type string 16 | MimeType string 17 | Usage string 18 | Name string 19 | Reference string 20 | sortable.Embed 21 | } 22 | 23 | func NewProductMedia() *ProductMedia { 24 | return &ProductMedia{} 25 | } 26 | 27 | type ProductMediaRepo interface { 28 | data.Repo[ProductMedia, string, interface{}] 29 | } 30 | -------------------------------------------------------------------------------- /product/private/biz/product_sku.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | kitgorm "github.com/go-saas/kit/pkg/gorm" 5 | sgorm "github.com/go-saas/saas/gorm" 6 | concurrency "github.com/goxiaoy/gorm-concurrency/v2" 7 | "time" 8 | ) 9 | 10 | // ProductSku sku 11 | type ProductSku struct { 12 | kitgorm.UIDBase 13 | kitgorm.AuditedModel 14 | concurrency.HasVersion 15 | sgorm.MultiTenancy 16 | 17 | ProductId string 18 | 19 | Title string 20 | 21 | MainPic *ProductMedia `gorm:"foreignKey:MainPicID"` 22 | MainPicID *string 23 | Medias []ProductMedia `gorm:"polymorphic:Owner;polymorphicValue:product_sku"` 24 | 25 | Prices []Price `gorm:"polymorphic:Owner;polymorphicValue:product_sku"` 26 | 27 | Stocks []Stock `gorm:"polymorphic:Owner;polymorphicValue:product_sku"` 28 | 29 | Keywords []Keyword `gorm:"polymorphic:Owner;polymorphicValue:product_sku;comment:商品关键字"` 30 | 31 | SaleableFrom *time.Time 32 | SaleableTo *time.Time 33 | Barcode string `gorm:"comment:商品条码"` 34 | } 35 | -------------------------------------------------------------------------------- /product/private/biz/seed.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/saas/seed" 6 | ) 7 | 8 | type ProductSeeder struct { 9 | } 10 | 11 | var _ seed.Contrib = (*ProductSeeder)(nil) 12 | 13 | func NewPostSeeder() *ProductSeeder { 14 | return &ProductSeeder{} 15 | } 16 | 17 | func (p *ProductSeeder) Seed(ctx context.Context, sCtx *seed.Context) error { 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /product/private/biz/sync_link.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import "time" 4 | 5 | type ProductSyncLink struct { 6 | ProductId string `gorm:"primary_key"` 7 | ProviderName string `gorm:"primary_key"` 8 | ProviderId string `gorm:"index"` 9 | LastSyncTime *time.Time 10 | } 11 | -------------------------------------------------------------------------------- /product/private/conf/conf.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package product.private.conf; 3 | 4 | option go_package = "github.com/go-saas/kit/product/private/conf;conf"; 5 | 6 | import "conf/conf.proto"; 7 | import "blob/blob.proto"; 8 | import "stripe/stripe.proto"; 9 | 10 | message Bootstrap { 11 | .conf.Data data = 2; 12 | .conf.Security security=3; 13 | .conf.Services services =4; 14 | .conf.Logging logging=6; 15 | .conf.Tracers tracing=7; 16 | .conf.AppConfig app=8; 17 | 18 | .stripe.Conf stripe=501; 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /product/private/data/README.md: -------------------------------------------------------------------------------- 1 | # Data 2 | -------------------------------------------------------------------------------- /product/private/data/db_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "gorm.io/driver/sqlite" 6 | "gorm.io/gorm" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | var db *gorm.DB 12 | 13 | func TestMain(m *testing.M) { 14 | var err error 15 | db, err = gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) 16 | if err != nil { 17 | panic(err) 18 | } 19 | db = db.Debug() 20 | 21 | exitCode := m.Run() 22 | os.Exit(exitCode) 23 | } 24 | 25 | func TestMigration(t *testing.T) { 26 | err := migrateDb(db) 27 | assert.NoError(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /product/private/data/media.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | kitgorm "github.com/go-saas/kit/pkg/gorm" 6 | "github.com/go-saas/kit/product/private/biz" 7 | sgorm "github.com/go-saas/saas/gorm" 8 | "github.com/goxiaoy/go-eventbus" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | type ProductMediaRepo struct { 13 | *kitgorm.Repo[biz.ProductMedia, string, interface{}] 14 | } 15 | 16 | func NewProductMediaRepo(dbProvider sgorm.DbProvider, eventbus *eventbus.EventBus) biz.ProductMediaRepo { 17 | res := &ProductMediaRepo{} 18 | res.Repo = kitgorm.NewRepo[biz.ProductMedia, string, interface{}](dbProvider, eventbus, res) 19 | return res 20 | } 21 | 22 | func (c *ProductMediaRepo) GetDb(ctx context.Context) *gorm.DB { 23 | return GetDb(ctx, c.Repo.DbProvider) 24 | } 25 | -------------------------------------------------------------------------------- /product/private/data/migrate.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | kitgorm "github.com/go-saas/kit/pkg/gorm" 6 | "github.com/go-saas/kit/product/private/biz" 7 | "github.com/go-saas/saas/seed" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type Migrate struct { 12 | data *Data 13 | } 14 | 15 | func NewMigrate(data *Data) *Migrate { 16 | return &Migrate{ 17 | data: data, 18 | } 19 | } 20 | 21 | func (m *Migrate) Seed(ctx context.Context, sCtx *seed.Context) error { 22 | //make sure database exists 23 | ctx = kitgorm.NewDbGuardianContext(ctx) 24 | db := GetDb(ctx, m.data.DbProvider) 25 | return migrateDb(db) 26 | } 27 | 28 | func migrateDb(db *gorm.DB) error { 29 | return db.AutoMigrate( 30 | &biz.Brand{}, &biz.BrandTrans{}, &biz.ProductCategory{}, 31 | &biz.Product{}, &biz.ProductMedia{}, &biz.Badge{}, &biz.Keyword{}, &biz.CampaignRule{}, 32 | &biz.Price{}, &biz.PriceCurrencyOption{}, &biz.PriceCurrencyOptionTier{}, &biz.PriceRecurring{}, &biz.PriceTier{}, 33 | &biz.ProductAttribute{}, &biz.Stock{}, 34 | &biz.ProductSku{}, 35 | &biz.ProductSyncLink{}) 36 | } 37 | -------------------------------------------------------------------------------- /product/private/server/job.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | klog "github.com/go-kratos/kratos/v2/log" 5 | "github.com/go-saas/kit/pkg/job" 6 | "github.com/go-saas/kit/product/private/biz" 7 | "github.com/hibiken/asynq" 8 | ) 9 | 10 | func NewJobServer(opt asynq.RedisConnOpt, log klog.Logger, handlers []*job.Handler) *job.Server { 11 | // declare to handle product queue jobs 12 | srv := job.NewServer(opt, job.WithQueues(map[string]int{ 13 | string(biz.ConnName): 1, 14 | })) 15 | srv.Use(job.TracingServer(), job.Logging(log)) 16 | job.RegisterHandlers(srv, handlers...) 17 | return srv 18 | } 19 | -------------------------------------------------------------------------------- /proto/errors/errors.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package errors; 4 | 5 | option go_package = "github.com/go-kratos/kratos/v2/errors;errors"; 6 | option java_multiple_files = true; 7 | option java_package = "com.github.kratos.errors"; 8 | option objc_class_prefix = "KratosErrors"; 9 | 10 | import "google/protobuf/descriptor.proto"; 11 | 12 | message Status { 13 | int32 code = 1; 14 | string reason = 2; 15 | string message = 3; 16 | map metadata = 4; 17 | }; 18 | 19 | extend google.protobuf.EnumOptions { 20 | int32 default_code = 1108; 21 | } 22 | 23 | extend google.protobuf.EnumValueOptions { 24 | int32 code = 1109; 25 | } -------------------------------------------------------------------------------- /proto/lbs/address.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package lbs; 4 | import "google/protobuf/struct.proto"; 5 | 6 | option go_package = "github.com/go-saas/lbs;lbs"; 7 | 8 | message Address{ 9 | 10 | string country=1; 11 | string region=2; 12 | //State or province 13 | string state=3; 14 | string city=4; 15 | string zip_code=5; 16 | string line1=6; 17 | string line2=7; 18 | string line3=8; 19 | 20 | //Geo geojson 21 | google.protobuf.Struct geo=100; 22 | } 23 | -------------------------------------------------------------------------------- /proto/readme.md: -------------------------------------------------------------------------------- 1 | # Third-party proto files -------------------------------------------------------------------------------- /proto/validate/README.md: -------------------------------------------------------------------------------- 1 | # protoc-gen-validate (PGV) 2 | 3 | * https://github.com/envoyproxy/protoc-gen-validate 4 | -------------------------------------------------------------------------------- /quickstart/configs/hydra/hydra.yml: -------------------------------------------------------------------------------- 1 | serve: 2 | cookies: 3 | same_site_mode: Lax 4 | 5 | urls: 6 | self: 7 | #publich hydra server 8 | issuer: http://localhost 9 | 10 | # consent: http://127.0.0.1:7090/user/consent 11 | # login: http://127.0.0.1:7090/user/login 12 | # logout: http://127.0.0.1:7090/user/logout 13 | consent: http://127.0.0.1/user/consent 14 | login: http://127.0.0.1/user/login 15 | logout: http://127.0.0.1/user/logout 16 | 17 | secrets: 18 | system: 19 | - youReallyNeedToChangeThis 20 | 21 | oidc: 22 | subject_identifiers: 23 | supported_types: 24 | - pairwise 25 | - public 26 | pairwise: 27 | salt: youReallyNeedToChangeThis 28 | -------------------------------------------------------------------------------- /quickstart/configs/otel/otel-collector-config.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/58ee30b6bbe6c940690c63d5f8882fc5e7b2895b/examples/demo/otel-collector-config.yaml 2 | 3 | receivers: 4 | otlp: 5 | protocols: 6 | grpc: 7 | http: # add OTLP HTTP Receiver,default port is 4318 8 | 9 | exporters: 10 | prometheus: 11 | endpoint: "0.0.0.0:8889" 12 | const_labels: 13 | label1: value1 14 | 15 | debug: 16 | 17 | otlp: 18 | endpoint: jaeger-all-in-one:4317 19 | tls: 20 | insecure: true 21 | 22 | processors: 23 | batch: 24 | 25 | extensions: 26 | health_check: 27 | pprof: 28 | endpoint: :1888 29 | zpages: 30 | endpoint: :55679 31 | 32 | service: 33 | extensions: [pprof, zpages, health_check] 34 | pipelines: 35 | traces: 36 | receivers: [otlp] 37 | processors: [batch] 38 | exporters: [debug, otlp] 39 | metrics: 40 | receivers: [otlp] 41 | processors: [batch] 42 | exporters: [debug, prometheus] -------------------------------------------------------------------------------- /quickstart/configs/payment.yaml: -------------------------------------------------------------------------------- 1 | stripe: 2 | is_test: true 3 | publish_key: stripe_test_publish_key 4 | private_key: stripe_test_key 5 | webhook_key: stripe_test_webhook_key 6 | price_tables: 7 | plan: stripe_test_plan_price 8 | -------------------------------------------------------------------------------- /quickstart/configs/saas.yaml: -------------------------------------------------------------------------------- 1 | saas: -------------------------------------------------------------------------------- /quickstart/configs/sys.yaml: -------------------------------------------------------------------------------- 1 | sys: 2 | apisix: 3 | endpoint: http://apisix:9280 4 | api_key: edd1c9f034335f136f87ad84b625c8f1 5 | -------------------------------------------------------------------------------- /quickstart/configs/user.yaml: -------------------------------------------------------------------------------- 1 | user: 2 | password_score_min: 0 3 | admin: 4 | username: admin 5 | password: "123456" 6 | idp: {} 7 | -------------------------------------------------------------------------------- /quickstart/demo/dtm/conf.yml: -------------------------------------------------------------------------------- 1 | 2 | Store: # specify which engine to store trans status 3 | Driver: 'mysql' 4 | Host: 'mysqld' 5 | User: 'root' 6 | Password: 'youShouldChangeThis' 7 | Port: 3306 8 | Db: 'dtm' 9 | 10 | MicroService: # grpc based microservice config 11 | Driver: 'dtm-driver-kratos' # name of the driver to handle register/discover 12 | Target: 'etcd://etcd:2379/dtmservice' # register dtm server to this url 13 | EndPoint: 'grpc://dtm:36790' 14 | 15 | 16 | AdminBasePath: "/dtm-ui" -------------------------------------------------------------------------------- /quickstart/demo/hydra/hydra.yml: -------------------------------------------------------------------------------- 1 | serve: 2 | cookies: 3 | same_site_mode: Lax 4 | 5 | urls: 6 | self: 7 | #publich hydra server 8 | issuer: https://saas.nihaosaoya.com/ 9 | 10 | consent: https://saas.nihaosaoya.com/user/consent 11 | login: https://saas.nihaosaoya.com/user/login 12 | logout: https://saas.nihaosaoya.com/user/logout 13 | 14 | secrets: 15 | system: 16 | - youReallyNeedToChangeThis 17 | 18 | oidc: 19 | subject_identifiers: 20 | supported_types: 21 | - pairwise 22 | - public 23 | pairwise: 24 | salt: youReallyNeedToChangeThis 25 | -------------------------------------------------------------------------------- /quickstart/demo/z.config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | host_display_name: GO SAAS KIT DEMO 3 | domain_format: ([-a-z0-9]+)\.saas\.nihaosaoya\.com 4 | 5 | data: 6 | vfs: 7 | - 8 | public_url: https://saas.nihaosaoya.com/assets 9 | mount_path: "/" 10 | os: 11 | dir: ".assets" -------------------------------------------------------------------------------- /quickstart/demo/z.sys.yaml: -------------------------------------------------------------------------------- 1 | sys: 2 | apisix: 3 | modules: 4 | - routes: 5 | demo-http2https: 6 | uri: /* 7 | hosts: ["saas.nihaosaoya.com"] 8 | priority: 200 9 | vars: [ [ "scheme","==","http" ] ] 10 | plugins: 11 | redirect: 12 | http_to_https: true -------------------------------------------------------------------------------- /realtime/.gitignore: -------------------------------------------------------------------------------- 1 | # Reference https://github.com/github/gitignore/blob/master/Go.gitignore 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 19 | *.o 20 | *.a 21 | *.so 22 | 23 | # OS General 24 | Thumbs.db 25 | .DS_Store 26 | 27 | # project 28 | *.cert 29 | *.key 30 | *.log 31 | bin/ 32 | 33 | # Develop tools 34 | .vscode/ 35 | .idea/ 36 | *.swp 37 | -------------------------------------------------------------------------------- /realtime/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20 AS builder 2 | 3 | COPY . /src 4 | WORKDIR /src 5 | 6 | RUN make -f ./realtime/Makefile build 7 | 8 | FROM debian:stable-slim 9 | 10 | RUN apt-get update && apt-get install -y --no-install-recommends \ 11 | ca-certificates \ 12 | netbase \ 13 | && rm -rf /var/lib/apt/lists/ \ 14 | && apt-get autoremove -y && apt-get autoclean -y 15 | 16 | COPY --from=builder /src/bin /app 17 | 18 | WORKDIR /app 19 | 20 | EXPOSE 8000 21 | EXPOSE 9000 22 | VOLUME /data/conf 23 | 24 | CMD ["./realtime", "-conf", "/data/conf"] 25 | -------------------------------------------------------------------------------- /realtime/Makefile: -------------------------------------------------------------------------------- 1 | GOPATH:=$(shell go env GOPATH) 2 | VERSION=$(shell git describe --tags --always) 3 | 4 | 5 | .PHONY: api 6 | # generate api proto 7 | api: 8 | cd .. && buf generate --path ./buf/realtime --template ./realtime/buf.gen.yaml 9 | 10 | .PHONY: build 11 | # build 12 | build: 13 | mkdir -p bin/ && go build -buildvcs=false -ldflags "-X main.Version=$(VERSION)" -o ./bin/ ./... 14 | 15 | 16 | .PHONY: all 17 | # generate all 18 | all: 19 | make api; 20 | 21 | 22 | # show help 23 | help: 24 | @echo '' 25 | @echo 'Usage:' 26 | @echo ' make [target]' 27 | @echo '' 28 | @echo 'Targets:' 29 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 30 | helpMessage = match(lastLine, /^# (.*)/); \ 31 | if (helpMessage) { \ 32 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 33 | helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \ 34 | printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \ 35 | } \ 36 | } \ 37 | { lastLine = $$0 }' $(MAKEFILE_LIST) 38 | 39 | .DEFAULT_GOAL := help 40 | -------------------------------------------------------------------------------- /realtime/api/gateway.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/apisix" 6 | ) 7 | 8 | //go:embed gateway.yaml 9 | var gateway []byte 10 | 11 | func init() { 12 | apisix.LoadFromYaml(gateway) 13 | } 14 | -------------------------------------------------------------------------------- /realtime/api/gateway.yaml: -------------------------------------------------------------------------------- 1 | routes: 2 | realtime-api: 3 | uris: ["/v1/realtime/*","/assets/realtime/*"] 4 | upstream_id: realtime-http 5 | enable_websocket: true 6 | -------------------------------------------------------------------------------- /realtime/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: buf.build/grpc-ecosystem/openapiv2:v2.15.2 4 | out: ./realtime/private/service/openapi 5 | opt: 6 | - allow_merge=true 7 | - proto3_optional_nullable=true 8 | - merge_file_name=api 9 | -------------------------------------------------------------------------------- /realtime/event/v1/event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package kratos.api; 3 | import "google/protobuf/timestamp.proto"; 4 | import "google/protobuf/struct.proto"; 5 | 6 | option go_package = "github.com/go-saas/kit/realtime/event/v1;v1"; 7 | 8 | enum NotificationLevel{ 9 | INFO = 0; 10 | WARNING=1000; 11 | ERROR=2000; 12 | } 13 | 14 | message NotificationEvent{ 15 | string tenant_id=2; 16 | string group=3; 17 | string title=4; 18 | string desc=5; 19 | // google.protobuf.Timestamp send_time=6; 20 | string image=7; 21 | string link=8; 22 | string source=9; 23 | repeated string user_ids=10; 24 | google.protobuf.Struct extra=11; 25 | NotificationLevel level=12; 26 | } -------------------------------------------------------------------------------- /realtime/i18n/embed/en-US.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/realtime/i18n/embed/en-US.toml -------------------------------------------------------------------------------- /realtime/i18n/embed/zh-CN.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-saas/kit/8e55a6f58fa1e5f3ae8d7aeff025b38f8fed8a93/realtime/i18n/embed/zh-CN.toml -------------------------------------------------------------------------------- /realtime/i18n/i18n.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "embed" 5 | "github.com/go-saas/kit/pkg/localize" 6 | ) 7 | 8 | var ( 9 | //go:embed embed 10 | f embed.FS 11 | ) 12 | 13 | func init() { 14 | localize.RegisterFileBundle(localize.FileBundle{Fs: f}) 15 | } 16 | -------------------------------------------------------------------------------- /realtime/private/biz/README.md: -------------------------------------------------------------------------------- 1 | # Biz 2 | -------------------------------------------------------------------------------- /realtime/private/biz/biz.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/go-saas/kit/pkg/dal" 5 | kitdi "github.com/go-saas/kit/pkg/di" 6 | ) 7 | 8 | // ProviderSet is biz providers. 9 | var ProviderSet = kitdi.NewSet() 10 | 11 | const ConnName dal.ConnName = "realtime" 12 | -------------------------------------------------------------------------------- /realtime/private/conf/conf.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package realtime.private; 3 | 4 | option go_package = "github.com/go-saas/kit/realtime/private/conf;conf"; 5 | 6 | import "conf/conf.proto"; 7 | import "blob/blob.proto"; 8 | 9 | message Bootstrap { 10 | conf.Data data = 2; 11 | conf.Security security=3; 12 | conf.Services services =4; 13 | conf.Logging logging=6; 14 | conf.Tracers tracing=7; 15 | conf.AppConfig app=8; 16 | conf.Dev dev=9; 17 | } 18 | 19 | message Realtime{ 20 | //NodeId for snowflake 21 | int32 node_id=1; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /realtime/private/data/README.md: -------------------------------------------------------------------------------- 1 | # Data 2 | -------------------------------------------------------------------------------- /realtime/private/data/migrate.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | kitgorm "github.com/go-saas/kit/pkg/gorm" 6 | "github.com/go-saas/kit/realtime/private/biz" 7 | "github.com/go-saas/saas/seed" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type Migrate struct { 12 | data *Data 13 | } 14 | 15 | func NewMigrate(data *Data) *Migrate { 16 | return &Migrate{ 17 | data: data, 18 | } 19 | } 20 | func (m *Migrate) Seed(ctx context.Context, sCtx *seed.Context) error { 21 | //make sure database exists 22 | ctx = kitgorm.NewDbGuardianContext(ctx) 23 | db := GetDb(ctx, m.data.DbProvider) 24 | return migrateDb(db) 25 | } 26 | 27 | func migrateDb(db *gorm.DB) error { 28 | return db.AutoMigrate(&biz.Notification{}) 29 | } 30 | -------------------------------------------------------------------------------- /realtime/private/server/centrifuge.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "github.com/centrifugal/centrifuge" 6 | "github.com/go-kratos/kratos/v2/transport" 7 | ) 8 | 9 | type Centrifuge struct { 10 | node *centrifuge.Node 11 | } 12 | 13 | func NewCentrifuge(node *centrifuge.Node) *Centrifuge { 14 | return &Centrifuge{node: node} 15 | } 16 | 17 | var _ transport.Server = (*Centrifuge)(nil) 18 | 19 | func (c *Centrifuge) Start(ctx context.Context) error { 20 | return c.node.Run() 21 | } 22 | 23 | func (c *Centrifuge) Stop(ctx context.Context) error { 24 | return c.node.Shutdown(ctx) 25 | } 26 | -------------------------------------------------------------------------------- /realtime/private/server/event.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | klog "github.com/go-kratos/kratos/v2/log" 5 | "github.com/go-saas/kit/event" 6 | "github.com/go-saas/kit/event/trace" 7 | kitconf "github.com/go-saas/kit/pkg/conf" 8 | "github.com/go-saas/kit/pkg/dal" 9 | uow2 "github.com/go-saas/uow" 10 | "github.com/goava/di" 11 | ) 12 | 13 | func NewEventServer( 14 | c *kitconf.Data, 15 | conn dal.ConnName, 16 | logger klog.Logger, 17 | uowMgr uow2.Manager, 18 | handlers []event.ConsumerHandler, 19 | container *di.Container, 20 | ) *event.ConsumerFactoryServer { 21 | e := c.Endpoints.GetEventMergedDefault(string(conn)) 22 | srv := event.NewConsumerFactoryServer(e, container) 23 | srv.Use(event.ConsumerRecover(event.WithLogger(logger)), trace.Receive(), event.Logging(logger), event.ConsumerUow(uowMgr)) 24 | for _, handler := range handlers { 25 | srv.Append(handler) 26 | } 27 | return srv 28 | } 29 | -------------------------------------------------------------------------------- /realtime/private/service/README.md: -------------------------------------------------------------------------------- 1 | # Service 2 | -------------------------------------------------------------------------------- /saas/.gitignore: -------------------------------------------------------------------------------- 1 | # Reference https://github.com/github/gitignore/blob/master/Go.gitignore 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 19 | *.o 20 | *.a 21 | *.so 22 | 23 | # OS General 24 | Thumbs.db 25 | .DS_Store 26 | 27 | # project 28 | *.cert 29 | *.key 30 | *.log 31 | bin/ 32 | 33 | # Develop tools 34 | .vscode/ 35 | .idea/ 36 | *.swp 37 | -------------------------------------------------------------------------------- /saas/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20 AS builder 2 | 3 | COPY . /src 4 | WORKDIR /src 5 | 6 | RUN make -f ./saas/Makefile build 7 | 8 | FROM debian:stable-slim 9 | 10 | RUN apt-get update && apt-get install -y --no-install-recommends \ 11 | ca-certificates \ 12 | netbase \ 13 | && rm -rf /var/lib/apt/lists/ \ 14 | && apt-get autoremove -y && apt-get autoclean -y 15 | 16 | COPY --from=builder /src/bin /app 17 | 18 | WORKDIR /app 19 | 20 | EXPOSE 8000 21 | EXPOSE 9000 22 | VOLUME /data/conf 23 | 24 | CMD ["./saas", "-conf", "/data/conf"] 25 | -------------------------------------------------------------------------------- /saas/Makefile: -------------------------------------------------------------------------------- 1 | GOPATH:=$(shell go env GOPATH) 2 | VERSION=$(shell git describe --tags --always) 3 | 4 | .PHONY: generate 5 | # generate 6 | generate: 7 | go generate ./... 8 | 9 | .PHONY: api 10 | # generate api proto 11 | api: 12 | cd .. && buf generate --path ./buf/saas --template ./saas/buf.gen.yaml 13 | 14 | .PHONY: build 15 | # build 16 | build: 17 | mkdir -p bin/ && go build -buildvcs=false -ldflags "-X main.Version=$(VERSION)" -o ./bin/ ./... 18 | 19 | .PHONY: all 20 | # generate all 21 | all: 22 | make api; 23 | make generate 24 | 25 | 26 | # show help 27 | help: 28 | @echo '' 29 | @echo 'Usage:' 30 | @echo ' make [target]' 31 | @echo '' 32 | @echo 'Targets:' 33 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 34 | helpMessage = match(lastLine, /^# (.*)/); \ 35 | if (helpMessage) { \ 36 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 37 | helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \ 38 | printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \ 39 | } \ 40 | } \ 41 | { lastLine = $$0 }' $(MAKEFILE_LIST) 42 | 43 | .DEFAULT_GOAL := help 44 | -------------------------------------------------------------------------------- /saas/README.md: -------------------------------------------------------------------------------- 1 | # Saas 2 | 3 | | - | - | 4 | |----------------|------------------------| 5 | | type | service module | 6 | | saas | :x: | 7 | | desc | manage/resolve tenants | 8 | 9 | Features 10 | - [x] management tenants 11 | - [x] provide remote tenant store api 12 | -------------------------------------------------------------------------------- /saas/api/gateway.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/apisix" 6 | ) 7 | 8 | //go:embed gateway.yaml 9 | var gateway []byte 10 | 11 | func init() { 12 | apisix.LoadFromYaml(gateway) 13 | } 14 | -------------------------------------------------------------------------------- /saas/api/gateway.yaml: -------------------------------------------------------------------------------- 1 | routes: 2 | saas-api: 3 | uris: ["/v1/saas/*","/assets/saas/*"] 4 | upstream_id: saas-http 5 | -------------------------------------------------------------------------------- /saas/api/permission.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/authz/authz" 6 | ) 7 | 8 | const ( 9 | ResourceTenant = "saas.tenant" 10 | ResourcePlan = "saas.plan" 11 | ) 12 | 13 | //go:embed permission.yaml 14 | var permission []byte 15 | 16 | func init() { 17 | authz.LoadFromYaml(permission) 18 | } 19 | -------------------------------------------------------------------------------- /saas/api/plan/v1/error_reason.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: saas/api/plan/v1/error_reason.proto 3 | 4 | package v1 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | 19 | "google.golang.org/protobuf/types/known/anypb" 20 | ) 21 | 22 | // ensure the imports are used 23 | var ( 24 | _ = bytes.MinRead 25 | _ = errors.New("") 26 | _ = fmt.Print 27 | _ = utf8.UTFMax 28 | _ = (*regexp.Regexp)(nil) 29 | _ = (*strings.Reader)(nil) 30 | _ = net.IPv4len 31 | _ = time.Duration(0) 32 | _ = (*url.URL)(nil) 33 | _ = (*mail.Address)(nil) 34 | _ = anypb.Any{} 35 | _ = sort.Sort 36 | ) 37 | -------------------------------------------------------------------------------- /saas/api/plan/v1/error_reason.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package saas.api.plan.v1; 4 | import "errors/errors.proto"; 5 | 6 | option go_package = "github.com/go-saas/kit/saas/api/plan/v1;v1"; 7 | 8 | enum ErrorReason { 9 | 10 | option (errors.default_code) = 500; 11 | 12 | DUPLICATE_PLAN_KEY = 0 [(errors.code) = 400]; 13 | } -------------------------------------------------------------------------------- /saas/api/tenant/v1/error_reason.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: saas/api/tenant/v1/error_reason.proto 3 | 4 | package v1 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | 19 | "google.golang.org/protobuf/types/known/anypb" 20 | ) 21 | 22 | // ensure the imports are used 23 | var ( 24 | _ = bytes.MinRead 25 | _ = errors.New("") 26 | _ = fmt.Print 27 | _ = utf8.UTFMax 28 | _ = (*regexp.Regexp)(nil) 29 | _ = (*strings.Reader)(nil) 30 | _ = net.IPv4len 31 | _ = time.Duration(0) 32 | _ = (*url.URL)(nil) 33 | _ = (*mail.Address)(nil) 34 | _ = anypb.Any{} 35 | _ = sort.Sort 36 | ) 37 | -------------------------------------------------------------------------------- /saas/api/tenant/v1/error_reason.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package saas.api.tenant.v1; 4 | import "errors/errors.proto"; 5 | 6 | option go_package = "github.com/go-saas/kit/saas/api/tenant/v1;v1"; 7 | 8 | enum ErrorReason { 9 | 10 | option (errors.default_code) = 500; 11 | 12 | DUPLICATE_TENANT_NAME = 0 [(errors.code) = 400]; 13 | TENANT_NOT_FOUND = 1[(errors.code) = 404]; 14 | TENANT_FORBIDDEN = 2[(errors.code) = 403]; 15 | TENANT_NOT_READY = 3[(errors.code) = 403]; 16 | 17 | ADMIN_IDENTITY_REQUIRED =4[(errors.code) = 400]; 18 | ADMIN_PASSWORD_REQUIRED =5[(errors.code) = 400]; 19 | 20 | ADMIN_USERNAME_INVALID =6[(errors.code) = 400]; 21 | ADMIN_EMAIL_INVALID =7[(errors.code) = 400]; 22 | } -------------------------------------------------------------------------------- /saas/api/tenant_store.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/pkg/api" 6 | v1 "github.com/go-saas/kit/saas/api/tenant/v1" 7 | "github.com/go-saas/saas" 8 | ) 9 | 10 | type TenantStore struct { 11 | srv v1.TenantInternalServiceServer 12 | } 13 | 14 | var _ saas.TenantStore = (*TenantStore)(nil) 15 | 16 | func NewTenantStore(srv v1.TenantInternalServiceServer) saas.TenantStore { 17 | return &TenantStore{srv: srv} 18 | } 19 | 20 | func (r *TenantStore) GetByNameOrId(ctx context.Context, nameOrId string) (*saas.TenantConfig, error) { 21 | //replace withe trusted environment to skip trusted check if in same process 22 | ctx = api.NewTrustedContext(ctx) 23 | tenant, err := r.srv.GetTenant(ctx, &v1.GetTenantRequest{IdOrName: nameOrId}) 24 | if err != nil { 25 | return nil, err 26 | } 27 | pk := "" 28 | if tenant.PlanKey != nil { 29 | pk = *tenant.PlanKey 30 | } 31 | ret := saas.NewTenantConfig(tenant.Id, tenant.Name, tenant.Region, pk) 32 | for _, conn := range tenant.Conn { 33 | ret.Conn[conn.Key] = conn.Value 34 | } 35 | return ret, nil 36 | } 37 | -------------------------------------------------------------------------------- /saas/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: buf.build/grpc-ecosystem/openapiv2:v2.15.2 4 | out: ./saas/private/service/openapi 5 | opt: 6 | - allow_merge=true 7 | - proto3_optional_nullable=true 8 | - merge_file_name=api 9 | -------------------------------------------------------------------------------- /saas/event/v1/event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package saas.event.v1; 4 | 5 | import "google/api/annotations.proto"; 6 | import "google/protobuf/field_mask.proto"; 7 | import "google/protobuf/timestamp.proto"; 8 | import "validate/validate.proto"; 9 | 10 | import "google/api/field_behavior.proto"; 11 | 12 | option go_package = "github.com/go-saas/kit/saas/event/v1;v1"; 13 | 14 | message TenantCreatedEvent{ 15 | string id=1; 16 | string name=2; 17 | string region=4; 18 | bool separate_db=6; 19 | 20 | string admin_email=7; 21 | string admin_username=8; 22 | string admin_password=9; 23 | string admin_user_id=10; 24 | } 25 | 26 | message TenantReadyEvent{ 27 | string id=1; 28 | string service_name=2; 29 | } -------------------------------------------------------------------------------- /saas/i18n/embed/en-US.toml: -------------------------------------------------------------------------------- 1 | [TenantNotFound] 2 | other = "Tenant Not Found" 3 | [DuplicateTenantName] 4 | other = "Tenant Name Used" 5 | [TenantForbidden] 6 | other = "Tenant Forbidden" 7 | 8 | [AdminUsernameInvalid] 9 | other = "Administrator Username Invalid" 10 | [AdminEmailInvalid] 11 | other = "Administrator Email Invalid" 12 | 13 | [permission.saas.tenant.group] 14 | other = "Tenant" 15 | [permission.saas.tenant.read] 16 | other = "Read Tenant" 17 | [permission.saas.tenant.create] 18 | other = "Create Tenant" 19 | [permission.saas.tenant.update] 20 | other = "Update Tenant" 21 | [permission.saas.tenant.delete] 22 | other = "Delete Tenant" 23 | 24 | [permission.saas.plan.group] 25 | other = "Plan" 26 | [permission.saas.plan.read] 27 | other = "Read Plan" 28 | [permission.saas.plan.create] 29 | other = "Create Plan" 30 | [permission.saas.plan.update] 31 | other = "Update Plan" 32 | [permission.saas.plan.delete] 33 | other = "Delete Plan" 34 | 35 | [saas.plan.management] 36 | other = "Plan Management" -------------------------------------------------------------------------------- /saas/i18n/embed/zh-CN.toml: -------------------------------------------------------------------------------- 1 | [TenantNotFound] 2 | other = "未找到该租户" 3 | [DuplicateTenantName] 4 | other = "租户名称已被使用" 5 | [TenantForbidden] 6 | other = "租户未授权" 7 | 8 | [AdminUsernameInvalid] 9 | other = "管理员用户名格式错误" 10 | [AdminEmailInvalid] 11 | other = "管理员电子邮箱格式错误" 12 | 13 | [permission.saas.tenant.group] 14 | other = "租户" 15 | [permission.saas.tenant.read] 16 | other = "读取租户" 17 | [permission.saas.tenant.create] 18 | other = "创建租户" 19 | [permission.saas.tenant.update] 20 | other = "更新租户" 21 | [permission.saas.tenant.delete] 22 | other = "删除租户" 23 | 24 | [permission.saas.plan.group] 25 | other = "计划" 26 | [permission.saas.plan.read] 27 | other = "读取计划" 28 | [permission.saas.plan.create] 29 | other = "创建计划" 30 | [permission.saas.plan.update] 31 | other = "更新计划" 32 | [permission.saas.plan.delete] 33 | other = "删除计划" 34 | 35 | [saas.plan.management] 36 | other = "计划管理" -------------------------------------------------------------------------------- /saas/i18n/i18n.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "embed" 5 | "github.com/go-saas/kit/pkg/localize" 6 | ) 7 | 8 | var ( 9 | //go:embed embed 10 | f embed.FS 11 | ) 12 | 13 | func init() { 14 | localize.RegisterFileBundle(localize.FileBundle{Fs: f}) 15 | } 16 | -------------------------------------------------------------------------------- /saas/private/biz/README.md: -------------------------------------------------------------------------------- 1 | # Biz 2 | -------------------------------------------------------------------------------- /saas/private/biz/biz.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/go-saas/kit/event" 5 | "github.com/go-saas/kit/pkg/dal" 6 | kitdi "github.com/go-saas/kit/pkg/di" 7 | "github.com/goava/di" 8 | ) 9 | 10 | // ProviderSet is biz providers. 11 | var ProviderSet = kitdi.NewSet( 12 | NewTenantUserCase, 13 | kitdi.NewProvider(NewTenantReadyEventHandler, di.As(new(event.ConsumerHandler))), 14 | kitdi.NewProvider(NewSubscriptionChangedEventHandler, di.As(new(event.ConsumerHandler))), 15 | kitdi.NewProvider(NewOrderChangedEventHandler, di.As(new(event.ConsumerHandler))), 16 | NewConfigConnStrGenerator, 17 | ) 18 | 19 | const ConnName dal.ConnName = "saas" 20 | 21 | const TenantLogoPath = "saas/tenant/logo" 22 | -------------------------------------------------------------------------------- /saas/private/biz/conn_str_generator.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/go-saas/kit/saas/private/conf" 7 | ) 8 | 9 | type ConnStrGenerator interface { 10 | //Generate connection string for tenant before creation 11 | Generate(ctx context.Context, tenant *Tenant) ([]TenantConn, error) 12 | } 13 | 14 | type ConfigConnStrGenerator struct { 15 | saasConf *conf.SaasConf 16 | } 17 | 18 | func NewConfigConnStrGenerator(saasConf *conf.SaasConf) ConnStrGenerator { 19 | return &ConfigConnStrGenerator{saasConf: saasConf} 20 | } 21 | 22 | func (c *ConfigConnStrGenerator) Generate(ctx context.Context, tenant *Tenant) ([]TenantConn, error) { 23 | var res []TenantConn 24 | if c.saasConf != nil { 25 | for _, template := range c.saasConf.Database { 26 | res = append(res, TenantConn{Key: template.Name, Value: fmt.Sprintf(template.Template, tenant.ID.String())}) 27 | } 28 | } 29 | return res, nil 30 | } 31 | -------------------------------------------------------------------------------- /saas/private/conf/conf.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package saas.internal; 3 | 4 | option go_package = "github.com/go-saas/kit/saas/private/conf;conf"; 5 | 6 | import "conf/conf.proto"; 7 | import "blob/blob.proto"; 8 | 9 | message Bootstrap { 10 | conf.Data data = 2; 11 | conf.Security security=3; 12 | conf.Services services =4; 13 | conf.Logging logging=6; 14 | conf.Tracers tracing=7; 15 | conf.AppConfig app=8; 16 | conf.Dev dev=9; 17 | 18 | SaasConf saas=20; 19 | 20 | } 21 | 22 | 23 | message SaasConf{ 24 | repeated DatabaseTemplate database = 1; 25 | conf.Cookie tenant_cookie=2; 26 | } 27 | 28 | message DatabaseTemplate{ 29 | string name=1; 30 | string template=2; 31 | 32 | } -------------------------------------------------------------------------------- /saas/private/data/README.md: -------------------------------------------------------------------------------- 1 | # Data 2 | -------------------------------------------------------------------------------- /saas/private/data/migrate.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | kitgorm "github.com/go-saas/kit/pkg/gorm" 6 | "github.com/go-saas/kit/saas/private/biz" 7 | "github.com/go-saas/saas/seed" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type Migrate struct { 12 | data *Data 13 | } 14 | 15 | func NewMigrate(data *Data) *Migrate { 16 | return &Migrate{ 17 | data: data, 18 | } 19 | } 20 | 21 | func (m *Migrate) Seed(ctx context.Context, sCtx *seed.Context) error { 22 | if len(sCtx.TenantId) > 0 { 23 | //host only migrate 24 | return nil 25 | } 26 | //make sure database exists 27 | ctx = kitgorm.NewDbGuardianContext(ctx) 28 | db := GetDb(ctx, m.data.DbProvider) 29 | return migrateDb(db) 30 | } 31 | 32 | func migrateDb(db *gorm.DB) error { 33 | if err := db.AutoMigrate( 34 | &biz.Tenant{}, 35 | &biz.TenantConn{}, 36 | &biz.TenantFeature{}, 37 | &biz.Plan{}, 38 | &biz.PlanFeature{}, 39 | ); err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /saas/private/server/event.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | klog "github.com/go-kratos/kratos/v2/log" 5 | "github.com/go-saas/kit/event" 6 | "github.com/go-saas/kit/event/trace" 7 | kitconf "github.com/go-saas/kit/pkg/conf" 8 | "github.com/go-saas/kit/pkg/dal" 9 | uow2 "github.com/go-saas/uow" 10 | "github.com/goava/di" 11 | ) 12 | 13 | func NewEventServer( 14 | c *kitconf.Data, 15 | conn dal.ConnName, 16 | logger klog.Logger, 17 | uowMgr uow2.Manager, 18 | handlers []event.ConsumerHandler, 19 | container *di.Container, 20 | ) *event.ConsumerFactoryServer { 21 | e := c.Endpoints.GetEventMergedDefault(string(conn)) 22 | srv := event.NewConsumerFactoryServer(e, container) 23 | srv.Use(event.ConsumerRecover(event.WithLogger(logger)), trace.Receive(), event.Logging(logger), event.ConsumerUow(uowMgr)) 24 | for _, handler := range handlers { 25 | srv.Append(handler) 26 | } 27 | return srv 28 | } 29 | -------------------------------------------------------------------------------- /saas/private/server/job.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | klog "github.com/go-kratos/kratos/v2/log" 5 | "github.com/go-saas/kit/pkg/job" 6 | "github.com/go-saas/kit/saas/private/biz" 7 | "github.com/hibiken/asynq" 8 | ) 9 | 10 | func NewJobServer(opt asynq.RedisConnOpt, log klog.Logger) *job.Server { 11 | srv := job.NewServer(opt, job.WithQueues(map[string]int{ 12 | string(biz.ConnName): 1, 13 | })) 14 | srv.Use(job.TracingServer(), job.Logging(log)) 15 | return srv 16 | } 17 | -------------------------------------------------------------------------------- /saas/private/service/README.md: -------------------------------------------------------------------------------- 1 | # Service 2 | -------------------------------------------------------------------------------- /sys/.gitignore: -------------------------------------------------------------------------------- 1 | # Reference https://github.com/github/gitignore/blob/master/Go.gitignore 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 19 | *.o 20 | *.a 21 | *.so 22 | 23 | # OS General 24 | Thumbs.db 25 | .DS_Store 26 | 27 | # project 28 | *.cert 29 | *.key 30 | *.log 31 | bin/ 32 | 33 | # Develop tools 34 | .vscode/ 35 | .idea/ 36 | *.swp 37 | -------------------------------------------------------------------------------- /sys/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20 AS builder 2 | 3 | COPY . /src 4 | WORKDIR /src 5 | 6 | RUN make -f ./sys/Makefile build 7 | 8 | FROM debian:stable-slim 9 | 10 | RUN apt-get update && apt-get install -y --no-install-recommends \ 11 | ca-certificates \ 12 | netbase \ 13 | && rm -rf /var/lib/apt/lists/ \ 14 | && apt-get autoremove -y && apt-get autoclean -y 15 | 16 | COPY --from=builder /src/bin /app 17 | 18 | WORKDIR /app 19 | 20 | VOLUME /data/conf 21 | 22 | CMD ["./sys", "-conf", "/data/conf"] 23 | -------------------------------------------------------------------------------- /sys/Makefile: -------------------------------------------------------------------------------- 1 | GOPATH:=$(shell go env GOPATH) 2 | VERSION=$(shell git describe --tags --always) 3 | 4 | .PHONY: api 5 | # generate api proto 6 | api: 7 | cd .. && buf generate --path ./buf/sys --path ./buf/oidc --template ./sys/buf.gen.yaml 8 | 9 | .PHONY: build 10 | # build 11 | build: 12 | mkdir -p bin/ && go build -buildvcs=false -ldflags "-X main.Version=$(VERSION)" -o ./bin/ ./... 13 | 14 | .PHONY: generate 15 | # generate 16 | generate: 17 | go generate ./... 18 | 19 | .PHONY: all 20 | # generate all 21 | all: 22 | make api; 23 | make generate; 24 | 25 | # show help 26 | help: 27 | @echo '' 28 | @echo 'Usage:' 29 | @echo ' make [target]' 30 | @echo '' 31 | @echo 'Targets:' 32 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 33 | helpMessage = match(lastLine, /^# (.*)/); \ 34 | if (helpMessage) { \ 35 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 36 | helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \ 37 | printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \ 38 | } \ 39 | } \ 40 | { lastLine = $$0 }' $(MAKEFILE_LIST) 41 | 42 | .DEFAULT_GOAL := help 43 | -------------------------------------------------------------------------------- /sys/README.md: -------------------------------------------------------------------------------- 1 | # Sys 2 | 3 | | - | - | 4 | |----------------|----------------| 5 | | type | service module | 6 | | saas | :x: | 7 | | desc | handle menu | 8 | 9 | -------------------------------------------------------------------------------- /sys/api/gateway.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/apisix" 6 | ) 7 | 8 | //go:embed gateway.yaml 9 | var gateway []byte 10 | 11 | func init() { 12 | apisix.LoadFromYaml(gateway) 13 | } 14 | -------------------------------------------------------------------------------- /sys/api/menu/v1/error_reason.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: sys/api/menu/v1/error_reason.proto 3 | 4 | package v1 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | 19 | "google.golang.org/protobuf/types/known/anypb" 20 | ) 21 | 22 | // ensure the imports are used 23 | var ( 24 | _ = bytes.MinRead 25 | _ = errors.New("") 26 | _ = fmt.Print 27 | _ = utf8.UTFMax 28 | _ = (*regexp.Regexp)(nil) 29 | _ = (*strings.Reader)(nil) 30 | _ = net.IPv4len 31 | _ = time.Duration(0) 32 | _ = (*url.URL)(nil) 33 | _ = (*mail.Address)(nil) 34 | _ = anypb.Any{} 35 | _ = sort.Sort 36 | ) 37 | -------------------------------------------------------------------------------- /sys/api/menu/v1/error_reason.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package sys.api.menu.v1; 4 | import "errors/errors.proto"; 5 | 6 | option go_package = "github.com/go-saas/kit/sys/api/menu/v1;v1"; 7 | 8 | enum ErrorReason { 9 | 10 | option (errors.default_code) = 500; 11 | 12 | MENU_NAME_DUPLICATE = 0 [(errors.code) = 400]; 13 | MENU_PRESERVED =1 [(errors.code) = 403]; 14 | } -------------------------------------------------------------------------------- /sys/api/permission.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/authz/authz" 6 | ) 7 | 8 | const ( 9 | ResourceMenu = "sys.menu" 10 | ResourceDev = "dev" 11 | 12 | ResourceDevJaeger = "dev.jaeger" 13 | 14 | ResourceDevJob = "dev.jobs" 15 | ) 16 | 17 | //go:embed permission.yaml 18 | var permission []byte 19 | 20 | func init() { 21 | authz.LoadFromYaml(permission) 22 | } 23 | -------------------------------------------------------------------------------- /sys/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: buf.build/grpc-ecosystem/openapiv2:v2.15.2 4 | out: ./sys/private/service/openapi 5 | opt: 6 | - allow_merge=true 7 | - proto3_optional_nullable=true 8 | - merge_file_name=api 9 | -------------------------------------------------------------------------------- /sys/i18n/i18n.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "embed" 5 | _ "github.com/go-saas/kit/oidc/i18n" 6 | "github.com/go-saas/kit/pkg/localize" 7 | ) 8 | 9 | var ( 10 | //go:embed embed 11 | f embed.FS 12 | ) 13 | 14 | func init() { 15 | localize.RegisterFileBundle(localize.FileBundle{Fs: f}) 16 | } 17 | -------------------------------------------------------------------------------- /sys/menu/menu.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import _ "embed" 4 | 5 | //go:embed menu.yaml 6 | var menuData []byte 7 | 8 | var ( 9 | seedMenus [][]byte 10 | ) 11 | 12 | func init() { 13 | LoadFromYaml(menuData) 14 | } 15 | func LoadFromYaml(data []byte) { 16 | seedMenus = append(seedMenus, data) 17 | } 18 | 19 | func WalkMenus(f func(menu []byte) error) error { 20 | for _, menu := range seedMenus { 21 | if err := f(menu); err != nil { 22 | return err 23 | } 24 | } 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /sys/private/biz/README.md: -------------------------------------------------------------------------------- 1 | # Biz 2 | -------------------------------------------------------------------------------- /sys/private/biz/apisix_migration_task.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/pkg/job" 6 | "github.com/hibiken/asynq" 7 | "time" 8 | ) 9 | 10 | const ( 11 | JobTypeApisixMigration = string(ConnName) + ":" + "apisix" + ":" + "migration" 12 | ) 13 | 14 | func NewApisixMigrationTask() *asynq.Task { 15 | return asynq.NewTask(JobTypeApisixMigration, nil, asynq.ProcessIn(time.Second), asynq.Queue(string(ConnName)), asynq.Retention(time.Hour*24*30)) 16 | } 17 | 18 | func NewApisixMigrationTaskHandler(seeder *ApisixSeed) *job.Handler { 19 | return job.NewHandlerFunc(JobTypeApisixMigration, func(ctx context.Context, t *asynq.Task) error { 20 | return seeder.Do(ctx) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /sys/private/biz/biz.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/go-saas/kit/pkg/dal" 5 | kitdi "github.com/go-saas/kit/pkg/di" 6 | ) 7 | 8 | // ProviderSet is biz providers. 9 | var ProviderSet = kitdi.NewSet(NewMenuSeed, NewApisixSeed, NewApisixMigrationTaskHandler) 10 | 11 | const ( 12 | ConnName dal.ConnName = "sys" 13 | ) 14 | -------------------------------------------------------------------------------- /sys/private/conf/conf.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package sys.internal; 3 | 4 | option go_package = "github.com/go-saas/kit/sys/private/conf;conf"; 5 | 6 | import "conf/conf.proto"; 7 | import "blob/blob.proto"; 8 | import "apisix/apisix.proto"; 9 | import "google/protobuf/struct.proto"; 10 | 11 | 12 | message Bootstrap { 13 | conf.Data data = 2; 14 | conf.Security security=3; 15 | conf.Services services =4; 16 | conf.Logging logging=6; 17 | conf.Tracers tracing=7; 18 | conf.AppConfig app=8; 19 | conf.Dev dev=9; 20 | SysConf sys=10; 21 | } 22 | 23 | message SysConf{ 24 | apisix.Config apisix=1; 25 | } -------------------------------------------------------------------------------- /sys/private/data/README.md: -------------------------------------------------------------------------------- 1 | # Data 2 | -------------------------------------------------------------------------------- /sys/private/data/migrate.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | kitgorm "github.com/go-saas/kit/pkg/gorm" 6 | 7 | "github.com/go-saas/kit/sys/private/biz" 8 | "github.com/go-saas/saas/seed" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | type Migrate struct { 13 | data *Data 14 | } 15 | 16 | func NewMigrate(data *Data) *Migrate { 17 | return &Migrate{ 18 | data: data, 19 | } 20 | } 21 | func (m *Migrate) Seed(ctx context.Context, sCtx *seed.Context) error { 22 | if len(sCtx.TenantId) > 0 { 23 | //host only migrate 24 | return nil 25 | } 26 | //make sure database exists 27 | ctx = kitgorm.NewDbGuardianContext(ctx) 28 | db := GetDb(ctx, m.data.DbProvider) 29 | return migrateDb(db) 30 | } 31 | 32 | func migrateDb(db *gorm.DB) error { 33 | return db.AutoMigrate(&biz.Menu{}, &biz.MenuPermissionRequirement{}) 34 | } 35 | -------------------------------------------------------------------------------- /sys/private/server/job.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | klog "github.com/go-kratos/kratos/v2/log" 5 | "github.com/go-saas/kit/pkg/job" 6 | "github.com/go-saas/kit/sys/private/biz" 7 | "github.com/hibiken/asynq" 8 | ) 9 | 10 | func NewJobServer(opt asynq.RedisConnOpt, log klog.Logger, handlers []*job.Handler) *job.Server { 11 | srv := job.NewServer(opt, job.WithQueues(map[string]int{ 12 | string(biz.ConnName): 1, 13 | })) 14 | srv.Use(job.TracingServer(), job.Logging(log)) 15 | job.RegisterHandlers(srv, handlers...) 16 | return srv 17 | } 18 | -------------------------------------------------------------------------------- /sys/private/service/README.md: -------------------------------------------------------------------------------- 1 | # Service 2 | -------------------------------------------------------------------------------- /sys/private/service/locale.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/pkg/localize" 6 | pb "github.com/go-saas/kit/sys/api/locale/v1" 7 | ) 8 | 9 | type LocaleService struct { 10 | pb.UnimplementedLocaleServiceServer 11 | } 12 | 13 | func NewLocaleService() *LocaleService { 14 | return &LocaleService{} 15 | } 16 | 17 | func (s *LocaleService) ListMessages(ctx context.Context, req *pb.ListMessageRequest) (*pb.ListMessageReply, error) { 18 | loc := localize.FromContext(ctx) 19 | allMsg := loc.GetBundle().GetMessageTemplates() 20 | var items []*pb.LocaleLanguage 21 | for tag, m := range allMsg { 22 | var msg []*pb.LocaleMessage 23 | for k, template := range m { 24 | if template == nil { 25 | msg = append(msg, &pb.LocaleMessage{Id: k, Other: ""}) 26 | continue 27 | } 28 | msg = append(msg, &pb.LocaleMessage{Id: template.ID, Other: template.Other}) 29 | } 30 | items = append(items, &pb.LocaleLanguage{ 31 | Name: tag.String(), 32 | Msg: msg, 33 | }) 34 | } 35 | return &pb.ListMessageReply{Items: items}, nil 36 | } 37 | -------------------------------------------------------------------------------- /user/.gitignore: -------------------------------------------------------------------------------- 1 | # Reference https://github.com/github/gitignore/blob/master/Go.gitignore 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 19 | *.o 20 | *.a 21 | *.so 22 | 23 | # OS General 24 | Thumbs.db 25 | .DS_Store 26 | 27 | # project 28 | *.cert 29 | *.key 30 | *.log 31 | bin/ 32 | 33 | # Develop tools 34 | .vscode/ 35 | .idea/ 36 | *.swp 37 | -------------------------------------------------------------------------------- /user/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20 AS builder 2 | 3 | COPY . /src 4 | WORKDIR /src 5 | 6 | RUN make -f ./user/Makefile build 7 | 8 | FROM debian:stable-slim 9 | 10 | RUN apt-get update && apt-get install -y --no-install-recommends \ 11 | ca-certificates \ 12 | netbase \ 13 | && rm -rf /var/lib/apt/lists/ \ 14 | && apt-get autoremove -y && apt-get autoclean -y 15 | 16 | COPY --from=builder /src/bin /app 17 | 18 | WORKDIR /app 19 | 20 | EXPOSE 8000 21 | EXPOSE 9000 22 | VOLUME /data/conf 23 | 24 | CMD ["./user", "-conf", "/data/conf"] 25 | -------------------------------------------------------------------------------- /user/Makefile: -------------------------------------------------------------------------------- 1 | GOPATH:=$(shell go env GOPATH) 2 | VERSION=$(shell git describe --tags --always) 3 | 4 | .PHONY: generate 5 | # generate 6 | generate: 7 | go generate ./... 8 | 9 | .PHONY: api 10 | # generate api proto 11 | api: 12 | cd .. && buf generate --path ./buf/user --path ./buf/event --path ./buf/dtm --template ./user/buf.gen.yaml 13 | 14 | .PHONY: build 15 | # build 16 | build: 17 | mkdir -p bin/ && go build -buildvcs=false -ldflags "-X main.Version=$(VERSION)" -o ./bin/ ./... 18 | 19 | .PHONY: all 20 | # generate all 21 | all: 22 | make api; 23 | make generate 24 | 25 | # show help 26 | help: 27 | @echo '' 28 | @echo 'Usage:' 29 | @echo ' make [target]' 30 | @echo '' 31 | @echo 'Targets:' 32 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 33 | helpMessage = match(lastLine, /^# (.*)/); \ 34 | if (helpMessage) { \ 35 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 36 | helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \ 37 | printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \ 38 | } \ 39 | } \ 40 | { lastLine = $$0 }' $(MAKEFILE_LIST) 41 | 42 | .DEFAULT_GOAL := help 43 | -------------------------------------------------------------------------------- /user/README.md: -------------------------------------------------------------------------------- 1 | # User 2 | 3 | | - | - | 4 | |----------------|---------------| 5 | | type | service module | 6 | | saas | :heavy_check_mark: | 7 | | desc | handle menu | 8 | -------------------------------------------------------------------------------- /user/api/auth/v1/auth.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import "google.golang.org/protobuf/proto" 4 | 5 | func (x *LoginAuthRequest) StringWithMask(mask string) string { 6 | ret := proto.Clone(x).(*LoginAuthRequest) 7 | ret.Password = mask 8 | return ret.String() 9 | } 10 | 11 | func (x *TokenRequest) StringWithMask(mask string) string { 12 | ret := proto.Clone(x).(*TokenRequest) 13 | ret.Password = mask 14 | return ret.String() 15 | } 16 | 17 | func (x *ValidatePasswordRequest) StringWithMask(mask string) string { 18 | ret := proto.Clone(x).(*ValidatePasswordRequest) 19 | ret.Password = mask 20 | return ret.String() 21 | } 22 | 23 | func (x *WebLoginAuthRequest) StringWithMask(mask string) string { 24 | ret := proto.Clone(x).(*WebLoginAuthRequest) 25 | ret.Password = mask 26 | return ret.String() 27 | } 28 | 29 | func (x *RegisterAuthRequest) StringWithMask(mask string) string { 30 | ret := proto.Clone(x).(*RegisterAuthRequest) 31 | ret.Password = mask 32 | ret.ConfirmPassword = mask 33 | return ret.String() 34 | } 35 | -------------------------------------------------------------------------------- /user/api/auth/v1/error_reason.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: user/api/auth/v1/error_reason.proto 3 | 4 | package v1 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | 19 | "google.golang.org/protobuf/types/known/anypb" 20 | ) 21 | 22 | // ensure the imports are used 23 | var ( 24 | _ = bytes.MinRead 25 | _ = errors.New("") 26 | _ = fmt.Print 27 | _ = utf8.UTFMax 28 | _ = (*regexp.Regexp)(nil) 29 | _ = (*strings.Reader)(nil) 30 | _ = net.IPv4len 31 | _ = time.Duration(0) 32 | _ = (*url.URL)(nil) 33 | _ = (*mail.Address)(nil) 34 | _ = anypb.Any{} 35 | _ = sort.Sort 36 | ) 37 | -------------------------------------------------------------------------------- /user/api/gateway.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/apisix" 6 | ) 7 | 8 | //go:embed gateway.yaml 9 | var gateway []byte 10 | 11 | func init() { 12 | apisix.LoadFromYaml(gateway) 13 | } 14 | -------------------------------------------------------------------------------- /user/api/gateway.yaml: -------------------------------------------------------------------------------- 1 | routes: 2 | user-api: 3 | uris: ["/v1/account/*","/v1/auth/*","/v1/permission/*","/v1/role*","/v1/user*","/assets/user/*"] 4 | upstream_id: user-http 5 | -------------------------------------------------------------------------------- /user/api/permission.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | _ "embed" 5 | "github.com/go-saas/kit/pkg/authz/authz" 6 | ) 7 | 8 | const ( 9 | ResourceUser = "user.user" 10 | ResourceRole = "user.role" 11 | 12 | ResourceAdminUser = "user.admin.user" 13 | ) 14 | 15 | //go:embed permission.yaml 16 | var permission []byte 17 | 18 | func init() { 19 | authz.LoadFromYaml(permission) 20 | } 21 | -------------------------------------------------------------------------------- /user/api/role/v1/error_reason.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: user/api/role/v1/error_reason.proto 3 | 4 | package v1 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | 19 | "google.golang.org/protobuf/types/known/anypb" 20 | ) 21 | 22 | // ensure the imports are used 23 | var ( 24 | _ = bytes.MinRead 25 | _ = errors.New("") 26 | _ = fmt.Print 27 | _ = utf8.UTFMax 28 | _ = (*regexp.Regexp)(nil) 29 | _ = (*strings.Reader)(nil) 30 | _ = net.IPv4len 31 | _ = time.Duration(0) 32 | _ = (*url.URL)(nil) 33 | _ = (*mail.Address)(nil) 34 | _ = anypb.Any{} 35 | _ = sort.Sort 36 | ) 37 | -------------------------------------------------------------------------------- /user/api/role/v1/error_reason.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package user.api.role.v1; 4 | import "errors/errors.proto"; 5 | 6 | option go_package = "github.com/go-saas/kit/user/api/role/v1;v1"; 7 | 8 | enum ErrorReason { 9 | 10 | option (errors.default_code) = 500; 11 | 12 | ROLE_PRESERVED = 0 [(errors.code) = 403]; 13 | ROLE_NAME_DUPLICATE = 1[(errors.code) = 400]; 14 | 15 | } -------------------------------------------------------------------------------- /user/api/user/v1/error_reason.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: user/api/user/v1/error_reason.proto 3 | 4 | package v1 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | 19 | "google.golang.org/protobuf/types/known/anypb" 20 | ) 21 | 22 | // ensure the imports are used 23 | var ( 24 | _ = bytes.MinRead 25 | _ = errors.New("") 26 | _ = fmt.Print 27 | _ = utf8.UTFMax 28 | _ = (*regexp.Regexp)(nil) 29 | _ = (*strings.Reader)(nil) 30 | _ = net.IPv4len 31 | _ = time.Duration(0) 32 | _ = (*url.URL)(nil) 33 | _ = (*mail.Address)(nil) 34 | _ = anypb.Any{} 35 | _ = sort.Sort 36 | ) 37 | -------------------------------------------------------------------------------- /user/api/user/v1/error_reason.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package user.api.user.v1; 4 | import "errors/errors.proto"; 5 | 6 | option go_package = "github.com/go-saas/kit/user/api/user/v1;v1"; 7 | 8 | enum ErrorReason { 9 | 10 | option (errors.default_code) = 500; 11 | 12 | CONFIRM_PASSWORD_MISMATCH = 0 [(errors.code) = 400]; 13 | 14 | PASSWORD_INSUFFICIENT_STRENGTH = 1 [(errors.code) = 400]; 15 | 16 | INVALID_PASSWORD=2 [(errors.code) = 400]; 17 | 18 | DUPLICATE_USERNAME=3 [(errors.code) = 400]; 19 | DUPLICATE_EMAIL=4 [(errors.code) = 400]; 20 | DUPLICATE_PHONE=5 [(errors.code) = 400]; 21 | 22 | INVALID_EMAIL = 6[(errors.code) = 400]; 23 | INVALID_PHONE = 7[(errors.code) = 400]; 24 | INVALID_USERNAME= 8[(errors.code) = 400]; 25 | 26 | USER_NOT_FOUND=9[(errors.code) = 404]; 27 | } -------------------------------------------------------------------------------- /user/api/user/v1/user.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import "google.golang.org/protobuf/proto" 4 | 5 | func (x *CreateUserRequest) StringWithMask(mask string) string { 6 | ret := proto.Clone(x).(*CreateUserRequest) 7 | ret.Password = mask 8 | ret.ConfirmPassword = mask 9 | return ret.String() 10 | } 11 | 12 | func (x *AdminUpdateUserRequest) StringWithMask(mask string) string { 13 | ret := proto.Clone(x).(*AdminUpdateUserRequest) 14 | if ret.User != nil { 15 | ret.User.Password = mask 16 | ret.User.ConfirmPassword = mask 17 | } 18 | 19 | return ret.String() 20 | } 21 | -------------------------------------------------------------------------------- /user/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: buf.build/grpc-ecosystem/openapiv2:v2.15.2 4 | out: ./user/private/service/openapi 5 | opt: 6 | - allow_merge=true 7 | - proto3_optional_nullable=true 8 | - merge_file_name=api 9 | -------------------------------------------------------------------------------- /user/event/v1/event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package user.event.v1; 4 | 5 | import "google/api/annotations.proto"; 6 | import "google/protobuf/field_mask.proto"; 7 | import "google/protobuf/timestamp.proto"; 8 | import "validate/validate.proto"; 9 | 10 | import "google/api/field_behavior.proto"; 11 | 12 | option go_package = "github.com/go-saas/kit/user/event/v1;v1"; 13 | 14 | message UserRoleChangeEvent{ 15 | string user_id=1; 16 | } -------------------------------------------------------------------------------- /user/i18n/i18n.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "embed" 5 | "github.com/go-saas/kit/pkg/localize" 6 | ) 7 | 8 | var ( 9 | //go:embed embed 10 | f embed.FS 11 | ) 12 | 13 | func init() { 14 | localize.RegisterFileBundle(localize.FileBundle{Fs: f}) 15 | } 16 | -------------------------------------------------------------------------------- /user/private/biz/README.md: -------------------------------------------------------------------------------- 1 | # Biz 2 | -------------------------------------------------------------------------------- /user/private/biz/biz.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/go-saas/kit/event" 5 | "github.com/go-saas/kit/pkg/dal" 6 | kitdi "github.com/go-saas/kit/pkg/di" 7 | "github.com/goava/di" 8 | ) 9 | 10 | const ConnName dal.ConnName = "user" 11 | 12 | // ProviderSet is biz providers. 13 | var ProviderSet = kitdi.NewSet( 14 | NewUserManager, 15 | NewSignInManager, 16 | NewUserValidator, 17 | NewRoleManager, 18 | NewLookupNormalizer, 19 | 20 | kitdi.NewProvider(NewOtpTokenProvider, di.As(new(OtpTokenProvider))), 21 | NewEmailTokenProvider, 22 | NewPhoneTokenProvider, 23 | NewPasswordHasher, 24 | NewPasswordValidator, 25 | NewRoleSeed, 26 | NewUserSeed, 27 | NewPermissionSeeder, 28 | NewEmailSender, 29 | 30 | kitdi.NewProvider(NewUserRoleChangeEventHandler, di.As(new(event.ConsumerHandler))), 31 | ) 32 | 33 | const UserAvatarPath = "user/avatar" 34 | -------------------------------------------------------------------------------- /user/private/biz/cache.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package user.private.biz.cache; 3 | 4 | option go_package = "github.com/go-saas/kit/user/private/biz;biz"; 5 | 6 | message ForgetPasswordTwoStepTokenPayload { 7 | string user_id=1; 8 | } 9 | 10 | message UserRoleCacheItem{ 11 | message UserRole{ 12 | string role_id=1; 13 | string tenant_id=2; 14 | } 15 | repeated UserRole role=1; 16 | } 17 | 18 | message WeChatMiniProgramLoginTwoStepTokenPayload { 19 | string app_id=1; 20 | string session_key=2; 21 | string open_id=3; 22 | string union_id=4; 23 | string step=5; 24 | } -------------------------------------------------------------------------------- /user/private/biz/otp.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "crypto/rand" 5 | ) 6 | 7 | const otpChars = "1234567890" 8 | const otpLength = 6 9 | 10 | func GenerateOtp() (string, error) { 11 | buffer := make([]byte, otpLength) 12 | _, err := rand.Read(buffer) 13 | if err != nil { 14 | return "", err 15 | } 16 | otpCharsLength := len(otpChars) 17 | for i := 0; i < otpLength; i++ { 18 | buffer[i] = otpChars[int(buffer[i])%otpCharsLength] 19 | } 20 | return string(buffer), nil 21 | } 22 | -------------------------------------------------------------------------------- /user/private/biz/password_hasher_test.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | "github.com/stretchr/testify/assert" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestHashPassword(t *testing.T) { 11 | ctx := context.Background() 12 | h := NewPasswordHasher() 13 | hash, err := h.HashPassword(ctx, nil, "password") 14 | assert.NoError(t, err) 15 | assert.True(t, strings.HasPrefix(hash, "$argon2id$v=19$m=65536,t=1,p=2$"), hash) 16 | } 17 | 18 | func TestVerifyHashedPassword(t *testing.T) { 19 | ctx := context.Background() 20 | h := NewPasswordHasher() 21 | hash, _ := h.HashPassword(ctx, nil, "password") 22 | r := h.VerifyHashedPassword(ctx, nil, hash, "password") 23 | assert.Equal(t, PasswordVerificationSuccess, r) 24 | rf := h.VerifyHashedPassword(ctx, nil, hash, "password1") 25 | assert.Equal(t, PasswordVerificationFail, rf) 26 | 27 | } 28 | -------------------------------------------------------------------------------- /user/private/biz/password_validator.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | v1 "github.com/go-saas/kit/user/api/user/v1" 6 | "github.com/go-saas/kit/user/private/conf" 7 | "github.com/nbutton23/zxcvbn-go" 8 | ) 9 | 10 | type PasswordValidator interface { 11 | // Validate password 12 | Validate(ctx context.Context, password string) error 13 | } 14 | 15 | type passwordValidator struct { 16 | config *conf.UserConf 17 | } 18 | 19 | func NewPasswordValidator(c *conf.UserConf) PasswordValidator { 20 | return &passwordValidator{ 21 | config: c, 22 | } 23 | } 24 | 25 | func (p *passwordValidator) Validate(ctx context.Context, password string) (err error) { 26 | if len(password) > 100 { 27 | password = password[:100] 28 | } 29 | 30 | strength := zxcvbn.PasswordStrength(password, []string{}) 31 | ok := strength.Score >= int(p.config.PasswordScoreMin) 32 | if !ok { 33 | return v1.ErrorPasswordInsufficientStrengthLocalized(ctx, nil, nil) 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /user/private/biz/role_seed.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/pkg/authz/authz" 6 | "github.com/go-saas/saas/seed" 7 | ) 8 | 9 | type RoleSeed struct { 10 | rm *RoleManager 11 | permission authz.PermissionManagementService 12 | } 13 | 14 | func NewRoleSeed(roleMgr *RoleManager, permission authz.PermissionManagementService) *RoleSeed { 15 | return &RoleSeed{rm: roleMgr, permission: permission} 16 | } 17 | 18 | func (r *RoleSeed) Seed(ctx context.Context, _ *seed.Context) error { 19 | seedRoles := []*Role{ 20 | { 21 | Name: Admin, 22 | IsPreserved: true, 23 | }, 24 | } 25 | for _, sr := range seedRoles { 26 | role, err := r.rm.FindByName(ctx, sr.Name) 27 | if err != nil { 28 | return err 29 | } 30 | if role == nil { 31 | if err := r.rm.Create(ctx, sr); err != nil { 32 | return err 33 | } 34 | } 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /user/private/biz/user_addr.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/pkg/data" 6 | "github.com/go-saas/kit/pkg/gorm" 7 | "github.com/go-saas/lbs" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | type UserAddress struct { 12 | gorm.UIDBase 13 | gorm.AuditedModel 14 | UserId uuid.UUID `json:"user_id" gorm:"index"` 15 | Phone string `json:"phone"` 16 | Usage string `json:"usage"` 17 | Prefer bool `json:"prefer"` 18 | Address lbs.AddressEntity `json:"address" gorm:"embedded"` 19 | Metadata data.JSONMap `json:"metadata"` 20 | } 21 | 22 | type UserAddressRepo interface { 23 | data.Repo[UserAddress, string, interface{}] 24 | FindByUser(ctx context.Context, userId string) ([]*UserAddress, error) 25 | SetPrefer(ctx context.Context, addr *UserAddress) error 26 | } 27 | -------------------------------------------------------------------------------- /user/private/biz/user_login.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import "github.com/google/uuid" 4 | 5 | type UserLogin struct { 6 | UserId uuid.UUID `gorm:"type:char(36);primaryKey" json:"user_id"` 7 | LoginProvider string `gorm:"primaryKey" json:"login_provider"` 8 | ProviderKey string `gorm:"index" json:"provider_key"` 9 | } 10 | -------------------------------------------------------------------------------- /user/private/biz/user_role.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/event" 6 | v12 "github.com/go-saas/kit/user/event/v1" 7 | "github.com/go-saas/saas/gorm" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | type UserRole struct { 12 | gorm.MultiTenancy `gorm:"primaryKey"` 13 | UserID uuid.UUID `gorm:"type:char(36);primaryKey"` 14 | RoleID uuid.UUID `gorm:"type:char(36);primaryKey"` 15 | } 16 | 17 | func NewUserRoleChangeEventHandler(um *UserManager) event.ConsumerHandler { 18 | msg := &v12.UserRoleChangeEvent{} 19 | return event.ProtoHandler[*v12.UserRoleChangeEvent](msg, event.HandlerFuncOf[*v12.UserRoleChangeEvent](func(ctx context.Context, msg *v12.UserRoleChangeEvent) error { 20 | return um.RemoveUserRoleCache(ctx, msg.UserId) 21 | })) 22 | } 23 | -------------------------------------------------------------------------------- /user/private/biz/user_setting.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/pkg/data" 6 | "github.com/go-saas/kit/pkg/gorm" 7 | v1 "github.com/go-saas/kit/user/api/account/v1" 8 | ) 9 | 10 | // UserSetting contains key/value pairs of user settings 11 | type UserSetting struct { 12 | gorm.UIDBase 13 | UserId string `json:"user_id" gorm:"index"` 14 | Key string `json:"key" gorm:"index"` 15 | Value data.Value `gorm:"embedded"` 16 | } 17 | 18 | type UpdateUserSetting struct { 19 | Key string 20 | //Value new value 21 | Value *data.Value 22 | Delete bool 23 | } 24 | 25 | type UserSettingRepo interface { 26 | data.Repo[UserSetting, string, *v1.GetSettingsRequest] 27 | FindByUser(ctx context.Context, userId string, query *v1.GetSettingsRequest) ([]*UserSetting, error) 28 | UpdateByUser(ctx context.Context, userId string, updateBatch []UpdateUserSetting) error 29 | } 30 | -------------------------------------------------------------------------------- /user/private/data/README.md: -------------------------------------------------------------------------------- 1 | # Data 2 | -------------------------------------------------------------------------------- /user/private/data/db_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "gorm.io/driver/sqlite" 6 | "gorm.io/gorm" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | var db *gorm.DB 12 | 13 | func TestMain(m *testing.M) { 14 | var err error 15 | db, err = gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) 16 | if err != nil { 17 | panic(err) 18 | } 19 | db = db.Debug() 20 | 21 | exitCode := m.Run() 22 | os.Exit(exitCode) 23 | } 24 | 25 | func TestMigration(t *testing.T) { 26 | err := migrateDb(db) 27 | assert.NoError(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /user/private/data/repo.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/saas/gorm" 6 | g "gorm.io/gorm" 7 | ) 8 | 9 | type Repo struct { 10 | DbProvider gorm.DbProvider 11 | } 12 | 13 | func (r *Repo) GetDb(ctx context.Context) *g.DB { 14 | return GetDb(ctx, r.DbProvider) 15 | } 16 | -------------------------------------------------------------------------------- /user/private/server/event.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | klog "github.com/go-kratos/kratos/v2/log" 5 | "github.com/go-saas/kit/event" 6 | "github.com/go-saas/kit/event/trace" 7 | kitconf "github.com/go-saas/kit/pkg/conf" 8 | "github.com/go-saas/kit/pkg/dal" 9 | uow2 "github.com/go-saas/uow" 10 | "github.com/goava/di" 11 | ) 12 | 13 | func NewEventServer( 14 | c *kitconf.Data, 15 | conn dal.ConnName, 16 | logger klog.Logger, 17 | uowMgr uow2.Manager, 18 | handlers []event.ConsumerHandler, 19 | container *di.Container, 20 | ) *event.ConsumerFactoryServer { 21 | e := c.Endpoints.GetEventMergedDefault(string(conn)) 22 | srv := event.NewConsumerFactoryServer(e, container) 23 | srv.Use(event.ConsumerRecover(event.WithLogger(logger)), trace.Receive(), event.Logging(logger), event.ConsumerUow(uowMgr)) 24 | for _, handler := range handlers { 25 | srv.Append(handler) 26 | } 27 | return srv 28 | } 29 | -------------------------------------------------------------------------------- /user/private/server/job.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | klog "github.com/go-kratos/kratos/v2/log" 5 | "github.com/go-saas/kit/pkg/job" 6 | "github.com/go-saas/kit/user/private/biz" 7 | "github.com/hibiken/asynq" 8 | ) 9 | 10 | func NewJobServer(opt asynq.RedisConnOpt, log klog.Logger, handlers []*job.Handler) *job.Server { 11 | srv := job.NewServer(opt, job.WithQueues(map[string]int{ 12 | string(biz.ConnName): 1, 13 | })) 14 | srv.Use(job.TracingServer(), job.Logging(log)) 15 | job.RegisterHandlers(srv, handlers...) 16 | return srv 17 | } 18 | -------------------------------------------------------------------------------- /user/private/service/README.md: -------------------------------------------------------------------------------- 1 | # Service 2 | -------------------------------------------------------------------------------- /user/private/service/user_role_contrib.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "github.com/go-saas/kit/pkg/authz/authz" 6 | "github.com/go-saas/kit/user/private/biz" 7 | "github.com/go-saas/saas/data" 8 | ) 9 | 10 | type UserRoleContrib struct { 11 | um *biz.UserManager 12 | } 13 | 14 | func NewUserRoleContrib(um *biz.UserManager) *UserRoleContrib { 15 | return &UserRoleContrib{um: um} 16 | } 17 | 18 | func (u *UserRoleContrib) Process(ctx context.Context, subject authz.Subject) ([]authz.Subject, error) { 19 | if us, ok := authz.ParseUserSubject(subject); ok { 20 | if us.GetUserId() != "" { 21 | //TODO ? 22 | ctx = data.NewMultiTenancyDataFilter(ctx, false) 23 | roles, err := u.um.GetUserRoleIds(ctx, us.GetUserId(), false) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | roleSubjects := make([]authz.Subject, len(roles)) 29 | for i, r := range roles { 30 | roleSubjects[i] = authz.NewRoleSubject(r.RoleId) 31 | } 32 | return roleSubjects, nil 33 | } 34 | } 35 | return nil, nil 36 | } 37 | 38 | var _ authz.SubjectContrib = (*UserRoleContrib)(nil) 39 | --------------------------------------------------------------------------------