feat: add tenant entry registration APIs
This commit is contained in:
@@ -227,6 +227,13 @@ type ThirdPartyConfigAdapter interface {
|
||||
ThirdPartyConfig(group string, keys ...string) (map[string]string, error)
|
||||
}
|
||||
|
||||
// TenantEntryAdapter stores public tenant entry data before a user is authenticated.
|
||||
type TenantEntryAdapter interface {
|
||||
PublicUploadCreate(*t.PublicUpload) (*t.PublicUpload, error)
|
||||
TenantApplicationCreate(*t.TenantApplication) (*t.TenantApplication, error)
|
||||
TenantEntryFeedbackCreate(*t.TenantEntryFeedback) (*t.TenantEntryFeedback, error)
|
||||
}
|
||||
|
||||
// TenantRoutedAdapter can return a tenant-specific adapter, e.g. for the
|
||||
// "one tenant, one database" deployment model.
|
||||
type TenantRoutedAdapter interface {
|
||||
|
||||
@@ -247,6 +247,9 @@ func (a *adapter) Open(jsonconfig json.RawMessage) error {
|
||||
if err = ensureIMRegistrationInviteControlTables(controlDB); err != nil {
|
||||
return errors.New("mysql adapter failed to ensure registration invite control schema: " + err.Error())
|
||||
}
|
||||
if err = ensureIMTenantEntryControlTables(controlDB); err != nil {
|
||||
return errors.New("mysql adapter failed to ensure tenant entry control schema: " + err.Error())
|
||||
}
|
||||
}
|
||||
if !databaseMissing {
|
||||
if err = ensureIMGroupBusinessTables(a.db); err != nil {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
# - Control database: hotgo
|
||||
# - im_tenant
|
||||
# - im_tenant_config
|
||||
# - im_public_uploads, im_tenant_applications and im_tenant_entry_feedbacks
|
||||
# for unauthenticated enterprise registration and tenant-entry feedback
|
||||
#
|
||||
# - Default tenant business database: tinode
|
||||
# - im_users, im_topics, im_subscriptions, im_messages and extension tables
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
//go:build mysql
|
||||
// +build mysql
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
t "github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
func (a *adapter) PublicUploadCreate(upload *t.PublicUpload) (*t.PublicUpload, error) {
|
||||
db := a.controlDB()
|
||||
if db == nil {
|
||||
return nil, t.ErrInternal
|
||||
}
|
||||
now := t.TimeNow()
|
||||
if upload.CreatedAt.IsZero() {
|
||||
upload.CreatedAt = now
|
||||
}
|
||||
if upload.UploadedAt.IsZero() {
|
||||
upload.UploadedAt = now
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err := db.ExecContext(ctx, `INSERT INTO im_public_uploads
|
||||
(file_id,purpose,mime_type,size,etag,location,expires_at,created_at,uploaded_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?)`,
|
||||
upload.FileID, upload.Purpose, upload.MimeType, upload.Size, upload.ETag, upload.Location,
|
||||
nullableTenantEntryTime(upload.ExpiresAt), upload.CreatedAt, upload.UploadedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return upload, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantApplicationCreate(application *t.TenantApplication) (*t.TenantApplication, error) {
|
||||
db := a.controlDB()
|
||||
if db == nil {
|
||||
return nil, t.ErrInternal
|
||||
}
|
||||
now := t.TimeNow()
|
||||
if application.Status == "" {
|
||||
application.Status = t.TenantApplicationStatusReceived
|
||||
}
|
||||
if application.EstimatedReviewHour <= 0 {
|
||||
application.EstimatedReviewHour = 24
|
||||
}
|
||||
if application.SubmittedAt.IsZero() {
|
||||
application.SubmittedAt = now
|
||||
}
|
||||
application.CreatedAt = now
|
||||
application.UpdatedAt = now
|
||||
clientContext, _ := json.Marshal(application.ClientContext)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err := db.ExecContext(ctx, `INSERT INTO im_tenant_applications
|
||||
(application_id,credit_code,enterprise_email,license_file_id,tenant_code_hint,client_context,idempotency_key,status,estimated_review_hour,submitted_at,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
application.ApplicationID, application.CreditCode, application.EnterpriseEmail, application.LicenseFileID,
|
||||
application.TenantCodeHint, string(clientContext), application.IdempotencyKey, application.Status,
|
||||
application.EstimatedReviewHour, application.SubmittedAt, now, now)
|
||||
if err != nil {
|
||||
return a.tenantApplicationByIdempotency(application.IdempotencyKey)
|
||||
}
|
||||
return a.tenantApplicationByIdempotency(application.IdempotencyKey)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantEntryFeedbackCreate(feedback *t.TenantEntryFeedback) (*t.TenantEntryFeedback, error) {
|
||||
db := a.controlDB()
|
||||
if db == nil {
|
||||
return nil, t.ErrInternal
|
||||
}
|
||||
now := t.TimeNow()
|
||||
if feedback.Status == "" {
|
||||
feedback.Status = t.TenantEntryFeedbackReceived
|
||||
}
|
||||
if feedback.SubmittedAt.IsZero() {
|
||||
feedback.SubmittedAt = now
|
||||
}
|
||||
feedback.CreatedAt = now
|
||||
feedback.UpdatedAt = now
|
||||
screenshots, _ := json.Marshal(feedback.ScreenshotFileIDs)
|
||||
clientContext, _ := json.Marshal(feedback.ClientContext)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err := db.ExecContext(ctx, `INSERT INTO im_tenant_entry_feedbacks
|
||||
(feedback_id,description,contact,tenant_code_hint,screenshot_file_ids,client_context,idempotency_key,status,submitted_at,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
feedback.FeedbackID, feedback.Description, feedback.Contact, feedback.TenantCodeHint,
|
||||
string(screenshots), string(clientContext), feedback.IdempotencyKey, feedback.Status,
|
||||
feedback.SubmittedAt, now, now)
|
||||
if err != nil {
|
||||
return a.tenantEntryFeedbackByIdempotency(feedback.IdempotencyKey)
|
||||
}
|
||||
return a.tenantEntryFeedbackByIdempotency(feedback.IdempotencyKey)
|
||||
}
|
||||
|
||||
func (a *adapter) tenantApplicationByIdempotency(idempotencyKey string) (*t.TenantApplication, error) {
|
||||
db := a.controlDB()
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
row := struct {
|
||||
ApplicationID string `db:"application_id"`
|
||||
CreditCode string `db:"credit_code"`
|
||||
EnterpriseEmail string `db:"enterprise_email"`
|
||||
LicenseFileID string `db:"license_file_id"`
|
||||
TenantCodeHint string `db:"tenant_code_hint"`
|
||||
ClientContext []byte `db:"client_context"`
|
||||
IdempotencyKey string `db:"idempotency_key"`
|
||||
Status string `db:"status"`
|
||||
EstimatedReviewHour int `db:"estimated_review_hour"`
|
||||
SubmittedAt time.Time `db:"submitted_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}{}
|
||||
if err := db.GetContext(ctx, &row, `SELECT application_id,credit_code,enterprise_email,license_file_id,
|
||||
tenant_code_hint,client_context,idempotency_key,status,estimated_review_hour,submitted_at,created_at,updated_at
|
||||
FROM im_tenant_applications WHERE idempotency_key=?`, idempotencyKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
app := &t.TenantApplication{
|
||||
ApplicationID: row.ApplicationID,
|
||||
CreditCode: row.CreditCode,
|
||||
EnterpriseEmail: row.EnterpriseEmail,
|
||||
LicenseFileID: row.LicenseFileID,
|
||||
TenantCodeHint: row.TenantCodeHint,
|
||||
IdempotencyKey: row.IdempotencyKey,
|
||||
Status: row.Status,
|
||||
EstimatedReviewHour: row.EstimatedReviewHour,
|
||||
SubmittedAt: row.SubmittedAt,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
_ = json.Unmarshal(row.ClientContext, &app.ClientContext)
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (a *adapter) tenantEntryFeedbackByIdempotency(idempotencyKey string) (*t.TenantEntryFeedback, error) {
|
||||
db := a.controlDB()
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
row := struct {
|
||||
FeedbackID string `db:"feedback_id"`
|
||||
Description string `db:"description"`
|
||||
Contact string `db:"contact"`
|
||||
TenantCodeHint string `db:"tenant_code_hint"`
|
||||
ScreenshotFileIDs []byte `db:"screenshot_file_ids"`
|
||||
ClientContext []byte `db:"client_context"`
|
||||
IdempotencyKey string `db:"idempotency_key"`
|
||||
Status string `db:"status"`
|
||||
SubmittedAt time.Time `db:"submitted_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}{}
|
||||
if err := db.GetContext(ctx, &row, `SELECT feedback_id,description,contact,tenant_code_hint,
|
||||
screenshot_file_ids,client_context,idempotency_key,status,submitted_at,created_at,updated_at
|
||||
FROM im_tenant_entry_feedbacks WHERE idempotency_key=?`, idempotencyKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
feedback := &t.TenantEntryFeedback{
|
||||
FeedbackID: row.FeedbackID,
|
||||
Description: row.Description,
|
||||
Contact: row.Contact,
|
||||
TenantCodeHint: row.TenantCodeHint,
|
||||
IdempotencyKey: row.IdempotencyKey,
|
||||
Status: row.Status,
|
||||
SubmittedAt: row.SubmittedAt,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
_ = json.Unmarshal(row.ScreenshotFileIDs, &feedback.ScreenshotFileIDs)
|
||||
_ = json.Unmarshal(row.ClientContext, &feedback.ClientContext)
|
||||
return feedback, nil
|
||||
}
|
||||
|
||||
func (a *adapter) controlDB() *sqlx.DB {
|
||||
if a.registryDB != nil {
|
||||
return a.registryDB
|
||||
}
|
||||
return a.db
|
||||
}
|
||||
|
||||
func nullableTenantEntryTime(value time.Time) any {
|
||||
if value.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//go:build mysql
|
||||
// +build mysql
|
||||
|
||||
package mysql
|
||||
|
||||
import "github.com/jmoiron/sqlx"
|
||||
|
||||
func ensureIMTenantEntryControlTables(db *sqlx.DB) error {
|
||||
statements := []string{
|
||||
`CREATE TABLE IF NOT EXISTS im_public_uploads(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
file_id VARCHAR(64) NOT NULL,
|
||||
purpose VARCHAR(64) NOT NULL,
|
||||
mime_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
size BIGINT NOT NULL DEFAULT 0,
|
||||
etag VARCHAR(128) NOT NULL DEFAULT '',
|
||||
location VARCHAR(2048) NOT NULL DEFAULT '',
|
||||
expires_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_public_uploads_file(file_id),
|
||||
INDEX idx_im_public_uploads_purpose(purpose, created_at),
|
||||
INDEX idx_im_public_uploads_expires(expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_tenant_applications(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
application_id VARCHAR(64) NOT NULL,
|
||||
credit_code VARCHAR(18) NOT NULL,
|
||||
enterprise_email VARCHAR(255) NOT NULL DEFAULT '',
|
||||
license_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
tenant_code_hint VARCHAR(64) NOT NULL DEFAULT '',
|
||||
client_context JSON,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
estimated_review_hour INT NOT NULL DEFAULT 24,
|
||||
submitted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_tenant_applications_app(application_id),
|
||||
UNIQUE INDEX uk_im_tenant_applications_idempotency(idempotency_key),
|
||||
INDEX idx_im_tenant_applications_credit(credit_code),
|
||||
INDEX idx_im_tenant_applications_status(status, id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS im_tenant_entry_feedbacks(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
feedback_id VARCHAR(64) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
contact VARCHAR(255) NOT NULL DEFAULT '',
|
||||
tenant_code_hint VARCHAR(64) NOT NULL DEFAULT '',
|
||||
screenshot_file_ids JSON,
|
||||
client_context JSON,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
submitted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_tenant_entry_feedbacks_feedback(feedback_id),
|
||||
UNIQUE INDEX uk_im_tenant_entry_feedbacks_idempotency(idempotency_key),
|
||||
INDEX idx_im_tenant_entry_feedbacks_status(status, id),
|
||||
INDEX idx_im_tenant_entry_feedbacks_hint(tenant_code_hint, id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//go:build mysql
|
||||
// +build mysql
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnsureTenantEntryControlTablesStatements(t *testing.T) {
|
||||
stmts := []string{
|
||||
`CREATE TABLE IF NOT EXISTS im_public_uploads(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
file_id VARCHAR(64) NOT NULL,
|
||||
purpose VARCHAR(64) NOT NULL,
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_public_uploads_file(file_id)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS im_tenant_applications(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
application_id VARCHAR(64) NOT NULL,
|
||||
credit_code VARCHAR(18) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
UNIQUE INDEX uk_im_tenant_applications_idempotency(idempotency_key)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS im_tenant_entry_feedbacks(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
feedback_id VARCHAR(64) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
UNIQUE INDEX uk_im_tenant_entry_feedbacks_idempotency(idempotency_key)
|
||||
)`,
|
||||
}
|
||||
joined := strings.ToLower(strings.Join(stmts, "\n"))
|
||||
for _, want := range []string{"im_public_uploads", "im_tenant_applications", "im_tenant_entry_feedbacks", "idempotency_key"} {
|
||||
if !strings.Contains(joined, want) {
|
||||
t.Fatalf("schema statement missing %s", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user