Establish secure account and managed AI backend

Provide the production foundation for Apple identity, immutable credits, referrals, integrity checks, managed providers, and hardened Docker deployment.
This commit is contained in:
Rocky
2026-08-16 14:46:23 +08:00
commit 0af35d44f4
124 changed files with 21052 additions and 0 deletions
@@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYw
JAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwK
QXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNa
Fw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlv
biBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9y
bmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdh
NbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9au
Yen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/
MB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYw
CgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn
53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijV
oyFraWVIyd/dganmrduC1bmTBGwD
-----END CERTIFICATE-----
+61
View File
@@ -0,0 +1,61 @@
ktor:
application:
modules:
- com.osglab.account.ApplicationKt.module
deployment:
host: 0.0.0.0
port: "$PORT:8080"
app:
environment: "$APP_ENV:development"
publicBaseUrl: "$PUBLIC_BASE_URL:https://account.osglab.com"
inviteBaseUrl: "$INVITE_BASE_URL:https://osglab.com/i"
appStoreUrl: "$APP_STORE_URL:https://apps.apple.com/app/id0000000000"
database:
jdbcUrl: "$DATABASE_URL:jdbc:mysql://localhost:3306/osg_account?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&forceConnectionTimeZoneToSession=true"
username: "$DATABASE_USER:osg_account"
password: "$DATABASE_PASSWORD"
migrationUsername: "$DATABASE_MIGRATION_USER:"
migrationPassword: "$DATABASE_MIGRATION_PASSWORD:"
maximumPoolSize: "$DATABASE_POOL_SIZE:10"
session:
issuer: "$JWT_ISSUER:https://account.osglab.com"
audience: "$JWT_AUDIENCE:osgkeyboard-ios"
secret: "$JWT_SECRET"
accessMinutes: "$ACCESS_TOKEN_MINUTES:15"
refreshDays: "$REFRESH_TOKEN_DAYS:30"
gatewayGrantDays: "$GATEWAY_GRANT_DAYS:30"
encryption:
keyBase64: "$FIELD_ENCRYPTION_KEY"
antiAbuse:
identityHmacKeyBase64: "$IDENTITY_HMAC_KEY"
tombstoneRetentionDays: "$IDENTITY_TOMBSTONE_RETENTION_DAYS:365"
apple:
teamId: "$APPLE_TEAM_ID:"
keyId: "$APPLE_KEY_ID:"
clientId: "$APPLE_CLIENT_ID:com.osgkeyboard.ios"
privateKeyPem: "$APPLE_PRIVATE_KEY_PEM:"
jwksUrl: "$APPLE_JWKS_URL:https://appleid.apple.com/auth/keys"
tokenUrl: "$APPLE_TOKEN_URL:https://appleid.apple.com/auth/token"
revokeUrl: "$APPLE_REVOKE_URL:https://appleid.apple.com/auth/revoke"
credits:
signupTrial: "$SIGNUP_TRIAL_CREDITS:1000"
referralInviter: "$REFERRAL_INVITER_CREDITS:3000"
referralInvitee: "$REFERRAL_INVITEE_CREDITS:3000"
referralBindingDays: "$REFERRAL_BINDING_DAYS:7"
providers:
volcengine:
endpoint: "$VOLCENGINE_ASR_ENDPOINT:wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async"
appId: "$VOLCENGINE_APP_ID:"
accessToken: "$VOLCENGINE_ACCESS_TOKEN:"
apiKey: "$VOLCENGINE_API_KEY:"
resourceId: "$VOLCENGINE_RESOURCE_ID:volc.seedasr.sauc.duration"
deepseek:
endpoint: "$DEEPSEEK_ENDPOINT:https://api.deepseek.com/v1"
apiKey: "$DEEPSEEK_API_KEY:"
model: "$DEEPSEEK_MODEL:deepseek-v4-flash"
integrity:
enforceDeviceCheck: "$ENFORCE_DEVICE_CHECK:false"
enforceAppAttest: "$ENFORCE_APP_ATTEST:false"
appleEnvironment: "$APPLE_INTEGRITY_ENVIRONMENT:development"
challengeLifetimeSeconds: "$APP_ATTEST_CHALLENGE_TTL_SECONDS:300"
@@ -0,0 +1,47 @@
CREATE TABLE accounts (
id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
apple_sub VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
created_at TIMESTAMP(6) NOT NULL,
updated_at TIMESTAMP(6) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_accounts_apple_sub (apple_sub)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE apple_credentials (
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
encrypted_refresh_token MEDIUMTEXT NOT NULL,
created_at TIMESTAMP(6) NOT NULL,
updated_at TIMESTAMP(6) NOT NULL,
PRIMARY KEY (account_id),
CONSTRAINT fk_apple_credentials_account
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE sessions (
id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
family_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
refresh_token_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
replaced_by_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
created_at TIMESTAMP(6) NOT NULL,
expires_at TIMESTAMP(6) NOT NULL,
revoked_at TIMESTAMP(6) NULL,
reuse_detected_at TIMESTAMP(6) NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_sessions_refresh_token_hash (refresh_token_hash),
KEY ix_sessions_account_id (account_id),
KEY ix_sessions_family_id (family_id),
KEY ix_sessions_account_active (account_id, revoked_at, expires_at),
CONSTRAINT fk_sessions_account
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
CONSTRAINT fk_sessions_replaced_by
FOREIGN KEY (replaced_by_id) REFERENCES sessions (id) ON DELETE SET NULL,
CONSTRAINT chk_sessions_expiry CHECK (expires_at > created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE apple_event_receipts (
event_id VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
event_type VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
received_at TIMESTAMP(6) NOT NULL,
PRIMARY KEY (event_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -0,0 +1,352 @@
CREATE TABLE credit_accounts (
user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
balance BIGINT NOT NULL DEFAULT 0,
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (user_id),
CONSTRAINT chk_credit_accounts_non_negative CHECK (balance >= 0)
) ENGINE = InnoDB;
CREATE TABLE credit_rate_versions (
id CHAR(36) NOT NULL,
kind VARCHAR(8) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
provider VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
model VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
effective_from DATETIME(6) NOT NULL,
effective_until DATETIME(6) NULL,
asr_credits_numerator BIGINT NULL,
asr_millis_denominator BIGINT NULL,
input_credits_numerator BIGINT NULL,
input_tokens_denominator BIGINT NULL,
output_credits_numerator BIGINT NULL,
output_tokens_denominator BIGINT NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uk_credit_rates_effective (
kind, provider, model, effective_from
),
INDEX idx_credit_rates_lookup (kind, provider, model, effective_from),
CONSTRAINT chk_credit_rates_interval
CHECK (effective_until IS NULL OR effective_until > effective_from),
CONSTRAINT chk_credit_rates_shape CHECK (
(
kind = 'ASR'
AND asr_credits_numerator > 0
AND asr_millis_denominator > 0
AND input_credits_numerator IS NULL
AND input_tokens_denominator IS NULL
AND output_credits_numerator IS NULL
AND output_tokens_denominator IS NULL
)
OR
(
kind = 'LLM'
AND asr_credits_numerator IS NULL
AND asr_millis_denominator IS NULL
AND input_credits_numerator > 0
AND input_tokens_denominator > 0
AND output_credits_numerator > 0
AND output_tokens_denominator > 0
)
)
) ENGINE = InnoDB;
CREATE TABLE credit_reservations (
id CHAR(36) NOT NULL,
user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
rate_version_id CHAR(36) NOT NULL,
provider VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
model VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
usage_kind VARCHAR(8) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
estimated_asr_millis BIGINT NULL,
estimated_input_tokens BIGINT NULL,
estimated_output_tokens BIGINT NULL,
actual_asr_millis BIGINT NULL,
actual_input_tokens BIGINT NULL,
actual_output_tokens BIGINT NULL,
reserved_credits BIGINT NOT NULL,
settled_credits BIGINT NULL,
status VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
managed_call BOOLEAN NOT NULL,
reserve_idempotency_key VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
settle_idempotency_key VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL,
release_idempotency_key VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL,
refund_idempotency_key VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uk_credit_reservation_reserve (user_id, reserve_idempotency_key),
UNIQUE KEY uk_credit_reservation_settle (user_id, settle_idempotency_key),
UNIQUE KEY uk_credit_reservation_release (user_id, release_idempotency_key),
UNIQUE KEY uk_credit_reservation_refund (user_id, refund_idempotency_key),
INDEX idx_credit_reservations_user_status (user_id, status),
CONSTRAINT fk_credit_reservations_rate
FOREIGN KEY (rate_version_id) REFERENCES credit_rate_versions (id) ON DELETE RESTRICT,
CONSTRAINT chk_credit_reservations_positive CHECK (reserved_credits > 0),
CONSTRAINT chk_credit_reservations_estimated_usage CHECK (
(
usage_kind = 'ASR'
AND estimated_asr_millis >= 0
AND estimated_input_tokens IS NULL
AND estimated_output_tokens IS NULL
)
OR
(
usage_kind = 'LLM'
AND estimated_asr_millis IS NULL
AND estimated_input_tokens >= 0
AND estimated_output_tokens >= 0
)
),
CONSTRAINT chk_credit_reservations_state CHECK (
(
status = 'RESERVED'
AND actual_asr_millis IS NULL
AND actual_input_tokens IS NULL
AND actual_output_tokens IS NULL
AND settled_credits IS NULL
AND settle_idempotency_key IS NULL
AND release_idempotency_key IS NULL
AND refund_idempotency_key IS NULL
)
OR
(
status = 'SETTLED'
AND settled_credits >= 0
AND settle_idempotency_key IS NOT NULL
AND release_idempotency_key IS NULL
AND refund_idempotency_key IS NULL
AND (
(
usage_kind = 'ASR'
AND actual_asr_millis >= 0
AND actual_input_tokens IS NULL
AND actual_output_tokens IS NULL
)
OR
(
usage_kind = 'LLM'
AND actual_asr_millis IS NULL
AND actual_input_tokens >= 0
AND actual_output_tokens >= 0
)
)
)
OR
(
status = 'RELEASED'
AND actual_asr_millis IS NULL
AND actual_input_tokens IS NULL
AND actual_output_tokens IS NULL
AND settled_credits IS NULL
AND settle_idempotency_key IS NULL
AND release_idempotency_key IS NOT NULL
AND refund_idempotency_key IS NULL
)
OR
(
status = 'REFUNDED'
AND settled_credits >= 0
AND settle_idempotency_key IS NOT NULL
AND release_idempotency_key IS NULL
AND refund_idempotency_key IS NOT NULL
AND (
(
usage_kind = 'ASR'
AND actual_asr_millis >= 0
AND actual_input_tokens IS NULL
AND actual_output_tokens IS NULL
)
OR
(
usage_kind = 'LLM'
AND actual_asr_millis IS NULL
AND actual_input_tokens >= 0
AND actual_output_tokens >= 0
)
)
)
)
) ENGINE = InnoDB;
CREATE TABLE referral_campaigns (
id CHAR(36) NOT NULL,
name VARCHAR(100) NOT NULL,
starts_at DATETIME(6) NOT NULL,
ends_at DATETIME(6) NULL,
binding_window_seconds BIGINT NOT NULL,
inviter_reward_credits BIGINT NOT NULL,
invitee_reward_credits BIGINT NOT NULL,
max_rewarded_bindings BIGINT NULL,
budget_credits BIGINT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
INDEX idx_referral_campaigns_active (enabled, starts_at, ends_at),
CONSTRAINT chk_referral_campaign_interval
CHECK (ends_at IS NULL OR ends_at > starts_at),
CONSTRAINT chk_referral_campaign_values CHECK (
binding_window_seconds > 0
AND inviter_reward_credits > 0
AND invitee_reward_credits > 0
AND inviter_reward_credits <= 9223372036854775807 - invitee_reward_credits
AND (max_rewarded_bindings IS NULL OR max_rewarded_bindings > 0)
AND (
budget_credits IS NULL
OR (
budget_credits >= inviter_reward_credits
AND budget_credits - inviter_reward_credits >= invitee_reward_credits
)
)
)
) ENGINE = InnoDB;
CREATE TABLE referral_campaign_budgets (
campaign_id CHAR(36) NOT NULL,
rewarded_bindings BIGINT NOT NULL DEFAULT 0,
spent_credits BIGINT NOT NULL DEFAULT 0,
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (campaign_id),
CONSTRAINT fk_referral_campaign_budget_campaign
FOREIGN KEY (campaign_id) REFERENCES referral_campaigns (id) ON DELETE CASCADE,
CONSTRAINT chk_referral_campaign_budget_non_negative
CHECK (rewarded_bindings >= 0 AND spent_credits >= 0)
) ENGINE = InnoDB;
INSERT INTO referral_campaigns (
id, name, starts_at, binding_window_seconds,
inviter_reward_credits, invitee_reward_credits, enabled
) VALUES (
'00000000-0000-0000-0000-000000000001',
'Default referral campaign',
'1970-01-01 00:00:00.000000',
604800,
3000,
3000,
TRUE
);
INSERT INTO referral_campaign_budgets (campaign_id)
VALUES ('00000000-0000-0000-0000-000000000001');
CREATE TABLE referral_codes (
id CHAR(36) NOT NULL,
owner_user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
campaign_id CHAR(36) NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001',
code VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uk_referral_codes_owner_campaign (owner_user_id, campaign_id),
UNIQUE KEY uk_referral_codes_code (code),
CONSTRAINT fk_referral_codes_campaign
FOREIGN KEY (campaign_id) REFERENCES referral_campaigns (id) ON DELETE RESTRICT
) ENGINE = InnoDB;
CREATE TABLE referral_bindings (
id CHAR(36) NOT NULL,
inviter_user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
invitee_user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
code_id CHAR(36) NOT NULL,
campaign_id CHAR(36) NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001',
bound_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
rewarded_at DATETIME(6) NULL,
reward_settlement_id CHAR(36) NULL,
reward_status VARCHAR(24) CHARACTER SET ascii COLLATE ascii_bin
NOT NULL DEFAULT 'PENDING',
PRIMARY KEY (id),
UNIQUE KEY uk_referral_bindings_invitee (invitee_user_id),
UNIQUE KEY uk_referral_bindings_settlement (reward_settlement_id),
INDEX idx_referral_bindings_inviter (inviter_user_id),
CONSTRAINT fk_referral_bindings_code
FOREIGN KEY (code_id) REFERENCES referral_codes (id) ON DELETE RESTRICT,
CONSTRAINT fk_referral_bindings_campaign
FOREIGN KEY (campaign_id) REFERENCES referral_campaigns (id) ON DELETE RESTRICT,
CONSTRAINT chk_referral_bindings_no_self CHECK (inviter_user_id <> invitee_user_id),
CONSTRAINT chk_referral_bindings_reward_pair CHECK (
(
reward_status IN ('PENDING', 'INELIGIBLE_BUDGET')
AND rewarded_at IS NULL
AND reward_settlement_id IS NULL
)
OR
(
reward_status = 'REWARDED'
AND rewarded_at IS NOT NULL
AND reward_settlement_id IS NOT NULL
)
)
) ENGINE = InnoDB;
CREATE TABLE credit_usage_records (
id CHAR(36) NOT NULL,
reservation_id CHAR(36) NOT NULL,
user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
rate_version_id CHAR(36) NOT NULL,
usage_kind VARCHAR(8) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
asr_millis BIGINT NULL,
input_tokens BIGINT NULL,
output_tokens BIGINT NULL,
charged_credits BIGINT NOT NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uk_credit_usage_reservation (reservation_id),
INDEX idx_credit_usage_user_created (user_id, created_at),
CONSTRAINT fk_credit_usage_reservation
FOREIGN KEY (reservation_id) REFERENCES credit_reservations (id) ON DELETE CASCADE,
CONSTRAINT fk_credit_usage_rate
FOREIGN KEY (rate_version_id) REFERENCES credit_rate_versions (id) ON DELETE RESTRICT,
CONSTRAINT fk_credit_usage_account
FOREIGN KEY (user_id) REFERENCES credit_accounts (user_id) ON DELETE CASCADE,
CONSTRAINT chk_credit_usage_credits CHECK (charged_credits >= 0),
CONSTRAINT chk_credit_usage_shape CHECK (
(
usage_kind = 'ASR'
AND asr_millis >= 0
AND input_tokens IS NULL
AND output_tokens IS NULL
)
OR
(
usage_kind = 'LLM'
AND asr_millis IS NULL
AND input_tokens >= 0
AND output_tokens >= 0
)
)
) ENGINE = InnoDB;
CREATE TABLE credit_ledger (
id CHAR(36) NOT NULL,
user_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
entry_type VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
amount_delta BIGINT NOT NULL,
balance_after BIGINT NOT NULL,
idempotency_key VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
reference_id CHAR(36) NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uk_credit_ledger_idempotency (user_id, idempotency_key),
INDEX idx_credit_ledger_user_created (user_id, created_at, id),
INDEX idx_credit_ledger_reference (reference_id),
CONSTRAINT chk_credit_ledger_balance CHECK (balance_after >= 0),
CONSTRAINT chk_credit_ledger_key_length
CHECK (CHAR_LENGTH(idempotency_key) BETWEEN 8 AND 128),
CONSTRAINT chk_credit_ledger_type CHECK (
entry_type IN (
'SIGNUP_TRIAL',
'MANUAL_GRANT',
'USAGE_RESERVE',
'USAGE_SETTLE',
'USAGE_RELEASE',
'USAGE_REFUND',
'REFERRAL_INVITER',
'REFERRAL_INVITEE',
'STOREKIT_PURCHASE',
'SUBSCRIPTION_GRANT'
)
)
) ENGINE = InnoDB;
-- The application repository exposes append-only operations for ledger, rate,
-- and usage tables. Production additionally grants the runtime user SELECT and
-- INSERT only on these tables. Avoiding stored triggers keeps migrations
-- compatible with binary-logged MySQL without granting global SUPER privilege.
@@ -0,0 +1,76 @@
CREATE TABLE provider_requests (
request_id VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
provider_id VARCHAR(64) NOT NULL,
capability VARCHAR(32) NOT NULL,
status VARCHAR(24) NOT NULL,
provider_request_id VARCHAR(128) NULL,
server_duration_millis BIGINT UNSIGNED NULL,
error_code VARCHAR(96) NULL,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
completed_at TIMESTAMP(6) NULL,
PRIMARY KEY (request_id),
INDEX idx_provider_requests_account_created (account_id, created_at),
INDEX idx_provider_requests_provider_created (provider_id, created_at),
CONSTRAINT fk_provider_requests_account
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE usage_records (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
request_id VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
meter VARCHAR(32) NOT NULL,
units BIGINT UNSIGNED NOT NULL,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uq_usage_records_request_meter (request_id, meter),
CONSTRAINT fk_usage_records_provider_request
FOREIGN KEY (request_id) REFERENCES provider_requests (request_id)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE gateway_grants (
id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
idempotency_key VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
expires_at TIMESTAMP(6) NOT NULL,
revoked_at TIMESTAMP(6) NULL,
created_at TIMESTAMP(6) NOT NULL,
updated_at TIMESTAMP(6) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_gateway_grants_account_idempotency (account_id, idempotency_key),
INDEX idx_gateway_grants_account_active (account_id, revoked_at, expires_at),
CONSTRAINT fk_gateway_grants_account
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE gateway_grant_scopes (
grant_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
capability VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
PRIMARY KEY (grant_id, capability),
CONSTRAINT fk_gateway_grant_scopes_grant
FOREIGN KEY (grant_id) REFERENCES gateway_grants (id) ON DELETE CASCADE,
CONSTRAINT chk_gateway_grant_scope
CHECK (capability IN ('POLISH', 'AI', 'AGENT', 'ASR'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE gateway_refresh_tokens (
id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
grant_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
family_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
token_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
replaced_by_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
rotation_idempotency_key VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NULL,
expires_at TIMESTAMP(6) NOT NULL,
revoked_at TIMESTAMP(6) NULL,
reuse_detected_at TIMESTAMP(6) NULL,
created_at TIMESTAMP(6) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_gateway_refresh_token_hash (token_hash),
INDEX idx_gateway_refresh_grant (grant_id),
INDEX idx_gateway_refresh_family (family_id),
CONSTRAINT fk_gateway_refresh_grant
FOREIGN KEY (grant_id) REFERENCES gateway_grants (id) ON DELETE CASCADE,
CONSTRAINT fk_gateway_refresh_replacement
FOREIGN KEY (replaced_by_id) REFERENCES gateway_refresh_tokens (id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,51 @@
CREATE TABLE devicecheck_trial_claims (
device_token_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
status VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
PRIMARY KEY (device_token_hash),
KEY ix_devicecheck_trial_account (account_id),
CONSTRAINT fk_devicecheck_trial_account
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
CONSTRAINT chk_devicecheck_trial_status
CHECK (status IN ('RESERVED', 'APPLE_MARKED', 'COMPLETED', 'REJECTED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE app_attest_challenges (
id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
key_id VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
purpose VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
challenge_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
status VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
expires_at DATETIME(6) NOT NULL,
consumed_at DATETIME(6) NULL,
created_at DATETIME(6) NOT NULL,
PRIMARY KEY (id),
KEY ix_app_attest_challenge_expiry (expires_at),
KEY ix_app_attest_challenge_account (account_id),
CONSTRAINT fk_app_attest_challenge_account
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
CONSTRAINT chk_app_attest_challenge_purpose
CHECK (purpose IN ('ATTESTATION', 'ASSERTION')),
CONSTRAINT chk_app_attest_challenge_status
CHECK (status IN ('ISSUED', 'CONSUMED', 'EXPIRED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE app_attest_keys (
key_id VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
public_key_base64 VARCHAR(512) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
receipt_base64 MEDIUMTEXT CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
sign_counter BIGINT NOT NULL DEFAULT 0,
account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
status VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
PRIMARY KEY (key_id),
KEY ix_app_attest_keys_account (account_id),
CONSTRAINT fk_app_attest_keys_account
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE SET NULL,
CONSTRAINT chk_app_attest_counter_non_negative CHECK (sign_counter >= 0),
CONSTRAINT chk_app_attest_key_status CHECK (status IN ('ACTIVE', 'REVOKED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -0,0 +1,113 @@
ALTER TABLE accounts
ADD COLUMN identity_fingerprint CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NULL,
ADD COLUMN anti_abuse_restricted BOOLEAN NOT NULL DEFAULT FALSE,
ADD UNIQUE KEY uq_accounts_identity_fingerprint (identity_fingerprint);
CREATE TABLE account_identity_tombstones (
identity_fingerprint CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
deleted_at DATETIME(6) NOT NULL,
expires_at DATETIME(6) NOT NULL,
PRIMARY KEY (identity_fingerprint),
INDEX ix_account_tombstones_expiry (expires_at),
CONSTRAINT chk_account_tombstone_interval CHECK (expires_at > deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE apple_revocation_outbox (
id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
encrypted_refresh_token MEDIUMTEXT NULL,
created_at DATETIME(6) NOT NULL,
next_attempt_at DATETIME(6) NOT NULL,
attempt_count INT NOT NULL DEFAULT 0,
completed_at DATETIME(6) NULL,
PRIMARY KEY (id),
INDEX ix_apple_revocation_pending (completed_at, next_attempt_at),
CONSTRAINT chk_apple_revocation_attempts CHECK (attempt_count >= 0),
CONSTRAINT chk_apple_revocation_token_lifecycle CHECK (
(completed_at IS NULL AND encrypted_refresh_token IS NOT NULL)
OR
(completed_at IS NOT NULL AND encrypted_refresh_token IS NULL)
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Existing V2/V3 tables predated account foreign keys. Remove only already-orphaned
-- mutable state before enforcing referential integrity. The immutable credit ledger
-- intentionally remains pseudonymized by its now-unmapped random account UUID.
DELETE ur
FROM usage_records ur
LEFT JOIN provider_requests pr ON pr.request_id = ur.request_id
WHERE pr.request_id IS NULL;
DELETE rb
FROM referral_bindings rb
LEFT JOIN accounts inviter ON inviter.id = rb.inviter_user_id
LEFT JOIN accounts invitee ON invitee.id = rb.invitee_user_id
LEFT JOIN referral_codes rc ON rc.id = rb.code_id
LEFT JOIN accounts code_owner ON code_owner.id = rc.owner_user_id
WHERE inviter.id IS NULL
OR invitee.id IS NULL
OR rc.id IS NULL
OR code_owner.id IS NULL
OR rc.owner_user_id <> rb.inviter_user_id;
DELETE rc
FROM referral_codes rc
LEFT JOIN accounts a ON a.id = rc.owner_user_id
WHERE a.id IS NULL;
DELETE cr
FROM credit_reservations cr
LEFT JOIN accounts a ON a.id = cr.user_id
WHERE a.id IS NULL;
DELETE ca
FROM credit_accounts ca
LEFT JOIN accounts a ON a.id = ca.user_id
WHERE a.id IS NULL;
DELETE ur
FROM usage_records ur
JOIN provider_requests pr ON pr.request_id = ur.request_id
LEFT JOIN accounts a ON a.id = pr.account_id
WHERE a.id IS NULL;
DELETE pr
FROM provider_requests pr
LEFT JOIN accounts a ON a.id = pr.account_id
WHERE a.id IS NULL;
DELETE gg
FROM gateway_grants gg
LEFT JOIN accounts a ON a.id = gg.account_id
WHERE a.id IS NULL;
ALTER TABLE credit_accounts
ADD CONSTRAINT fk_credit_accounts_user
FOREIGN KEY (user_id) REFERENCES accounts (id) ON DELETE CASCADE;
ALTER TABLE credit_reservations
ADD CONSTRAINT fk_credit_reservations_user
FOREIGN KEY (user_id) REFERENCES accounts (id) ON DELETE CASCADE;
ALTER TABLE referral_bindings
DROP FOREIGN KEY fk_referral_bindings_code;
ALTER TABLE referral_bindings
ADD CONSTRAINT fk_referral_bindings_code
FOREIGN KEY (code_id) REFERENCES referral_codes (id) ON DELETE CASCADE,
ADD CONSTRAINT fk_referral_bindings_inviter
FOREIGN KEY (inviter_user_id) REFERENCES accounts (id) ON DELETE CASCADE,
ADD CONSTRAINT fk_referral_bindings_invitee
FOREIGN KEY (invitee_user_id) REFERENCES accounts (id) ON DELETE CASCADE;
ALTER TABLE referral_codes
ADD COLUMN owner_identity_fingerprint CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NULL,
ADD CONSTRAINT fk_referral_codes_owner
FOREIGN KEY (owner_user_id) REFERENCES accounts (id) ON DELETE CASCADE;
ALTER TABLE usage_records
DROP FOREIGN KEY fk_usage_records_provider_request;
ALTER TABLE usage_records
ADD CONSTRAINT fk_usage_records_provider_request
FOREIGN KEY (request_id) REFERENCES provider_requests (request_id) ON DELETE CASCADE;
@@ -0,0 +1,65 @@
-- Make client request IDs idempotent within an account, not globally.
ALTER TABLE usage_records
DROP FOREIGN KEY fk_usage_records_provider_request,
DROP INDEX uq_usage_records_request_meter,
ADD COLUMN account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL AFTER id;
UPDATE usage_records ur
JOIN provider_requests pr ON pr.request_id = ur.request_id
SET ur.account_id = pr.account_id;
ALTER TABLE usage_records
MODIFY account_id VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL;
-- Normalize every legacy value before installing the binary status column and
-- stricter CHECK. Unknown historical states are held for manual review rather
-- than making a non-transactional MySQL migration fail halfway through.
UPDATE provider_requests
SET status = CASE UPPER(status)
WHEN 'SUCCEEDED' THEN 'SETTLED'
WHEN 'FAILED' THEN 'MANUAL_REVIEW'
WHEN 'CLAIMED' THEN 'CLAIMED'
WHEN 'STARTED' THEN 'STARTED'
WHEN 'SETTLEMENT_PENDING' THEN 'SETTLEMENT_PENDING'
WHEN 'SETTLED' THEN 'SETTLED'
WHEN 'RELEASED' THEN 'RELEASED'
WHEN 'MANUAL_REVIEW' THEN 'MANUAL_REVIEW'
ELSE 'MANUAL_REVIEW'
END;
ALTER TABLE provider_requests
ADD COLUMN reservation_id CHAR(36) NULL AFTER account_id,
ADD COLUMN usage_meter VARCHAR(32) NULL AFTER provider_request_id,
ADD COLUMN usage_units BIGINT UNSIGNED NULL AFTER usage_meter,
ADD COLUMN usage_input_units BIGINT UNSIGNED NULL AFTER usage_units,
ADD COLUMN usage_output_units BIGINT UNSIGNED NULL AFTER usage_input_units,
MODIFY COLUMN status VARCHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
DROP PRIMARY KEY,
ADD PRIMARY KEY (account_id, request_id),
ADD UNIQUE KEY uq_provider_requests_reservation (reservation_id),
ADD INDEX idx_provider_requests_status_created (status, created_at),
ADD CONSTRAINT chk_provider_request_status CHECK (
status IN (
'CLAIMED',
'STARTED',
'SETTLEMENT_PENDING',
'SETTLED',
'RELEASED',
'MANUAL_REVIEW'
)
);
ALTER TABLE usage_records
ADD UNIQUE KEY uq_usage_records_account_request_meter (account_id, request_id, meter),
ADD CONSTRAINT fk_usage_records_provider_request
FOREIGN KEY (account_id, request_id)
REFERENCES provider_requests (account_id, request_id)
ON DELETE CASCADE;
-- A rotated refresh row points to its replacement. RESTRICT can block the
-- account -> grant -> refresh cascade when an account is deleted.
ALTER TABLE gateway_refresh_tokens
DROP FOREIGN KEY fk_gateway_refresh_replacement,
ADD CONSTRAINT fk_gateway_refresh_replacement
FOREIGN KEY (replaced_by_id) REFERENCES gateway_refresh_tokens (id)
ON DELETE SET NULL;
@@ -0,0 +1,49 @@
-- Initial integer-credit rate cards. Future price changes must insert a new
-- immutable version and close the previous effective interval.
INSERT INTO credit_rate_versions (
id,
kind,
provider,
model,
effective_from,
effective_until,
asr_credits_numerator,
asr_millis_denominator,
created_at
) VALUES (
'10000000-0000-0000-0000-000000000001',
'ASR',
'volcengine-sauc-v3',
'volc.seedasr.sauc.duration',
'1970-01-01 00:00:00.000000',
NULL,
1,
1000,
UTC_TIMESTAMP(6)
);
INSERT INTO credit_rate_versions (
id,
kind,
provider,
model,
effective_from,
effective_until,
input_credits_numerator,
input_tokens_denominator,
output_credits_numerator,
output_tokens_denominator,
created_at
) VALUES (
'10000000-0000-0000-0000-000000000002',
'LLM',
'deepseek',
'deepseek-v4-flash',
'1970-01-01 00:00:00.000000',
NULL,
1,
100,
2,
100,
UTC_TIMESTAMP(6)
);
@@ -0,0 +1,17 @@
{
"applinks": {
"details": [
{
"appIDs": [
"{{APPLE_APP_ID}}"
],
"components": [
{
"/": "/i/*",
"comment": "Open first-party OSG invitation links in the app"
}
]
}
]
}
}
+184
View File
@@ -0,0 +1,184 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow,noarchive">
<meta name="color-scheme" content="light dark">
<title>OSG 邀请 / Invitation</title>
<style nonce="{{CSP_NONCE}}">
:root {
color-scheme: light dark;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f5f5f7;
color: #1d1d1f;
}
* { box-sizing: border-box; }
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
padding: max(20px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right))
max(20px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left));
}
main {
width: min(100%, 460px);
padding: clamp(24px, 7vw, 40px);
border: 1px solid #dedee3;
border-radius: 24px;
background: #fff;
text-align: center;
box-shadow: 0 16px 48px rgb(0 0 0 / 8%);
}
.language-switcher {
display: flex;
justify-content: flex-end;
gap: 6px;
margin: -8px -8px 20px 0;
}
.language-button {
min-height: 36px;
padding: 7px 11px;
border: 1px solid #d2d2d7;
border-radius: 999px;
background: transparent;
color: inherit;
font: inherit;
font-size: .82rem;
cursor: pointer;
}
.language-button[aria-pressed="true"] {
border-color: #1d1d1f;
background: #1d1d1f;
color: #fff;
}
h1 { margin: 0 0 12px; font-size: clamp(1.65rem, 7vw, 2.25rem); line-height: 1.15; }
p { margin: 10px 0; color: #6e6e73; line-height: 1.55; }
code {
display: block;
margin: 24px 0;
padding: 17px 10px;
overflow-wrap: anywhere;
border: 1px solid #e5e5ea;
border-radius: 14px;
background: #f5f5f7;
color: #1d1d1f;
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
font-size: clamp(1rem, 4.8vw, 1.35rem);
letter-spacing: .05em;
user-select: all;
}
.actions { display: grid; gap: 12px; }
.action {
display: grid;
min-height: 50px;
place-items: center;
padding: 13px 18px;
border: 1px solid transparent;
border-radius: 14px;
font: inherit;
font-weight: 650;
cursor: pointer;
text-decoration: none;
}
.primary { background: #0071e3; color: #fff; }
.secondary { border-color: #d2d2d7; background: #fff; color: #1d1d1f; }
.store-link { background: #1d1d1f; color: #fff; }
.action:focus-visible, .language-button:focus-visible {
outline: 3px solid #69aaf5;
outline-offset: 3px;
}
.hint { margin-top: 22px; font-size: .88rem; }
#copy-status { min-height: 1.4em; margin-bottom: 0; font-size: .9rem; }
[data-language] { display: none; }
html[lang="zh-CN"] [data-language="zh-CN"],
html[lang="en"] [data-language="en"] { display: inline; }
@media (prefers-color-scheme: dark) {
:root { background: #101214; color: #f4f5f7; }
main { border-color: #30343a; background: #1a1d21; box-shadow: none; }
p { color: #b5bbc4; }
code { border-color: #3a3f46; background: #282c32; color: #f4f5f7; }
.language-button { border-color: #555b64; }
.language-button[aria-pressed="true"] { border-color: #f4f5f7; background: #f4f5f7; color: #17191c; }
.secondary { border-color: #555b64; background: #282c32; color: #f4f5f7; }
.store-link { background: #f4f5f7; color: #17191c; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; }
}
</style>
</head>
<body>
<main>
<nav class="language-switcher" aria-label="Language / 语言">
<button class="language-button" id="language-zh" type="button" aria-pressed="true">中文</button>
<button class="language-button" id="language-en" type="button" aria-pressed="false">English</button>
</nav>
<h1>
<span data-language="zh-CN">加入 OSG</span>
<span data-language="en">Join OSG</span>
</h1>
<p>
<span data-language="zh-CN">使用此邀请码开始体验。</span>
<span data-language="en">Use this invitation code to get started.</span>
</p>
<code id="invite-code" aria-label="邀请码 / Invitation code">{{INVITE_CODE}}</code>
<div class="actions">
<button class="action primary" id="copy-button" type="button">
<span data-language="zh-CN">复制邀请码</span>
<span data-language="en">Copy invitation code</span>
</button>
<a class="action secondary" href="{{UNIVERSAL_LINK}}" rel="noopener">
<span data-language="zh-CN">打开 App</span>
<span data-language="en">Open App</span>
</a>
<a class="action store-link" href="{{APP_STORE_URL}}" rel="noopener noreferrer">
<span data-language="zh-CN">前往 App Store</span>
<span data-language="en">Download on the App Store</span>
</a>
</div>
<p id="copy-status" role="status" aria-live="polite"></p>
<p class="hint">
<span data-language="zh-CN">若“打开 App”仍停留在浏览器,请从信息或邮件中再次轻点原邀请链接。</span>
<span data-language="en">If “Open App” stays in the browser, tap the original invitation link again from Messages or Mail.</span>
</p>
</main>
<script nonce="{{CSP_NONCE}}">
const copyButton = document.getElementById("copy-button");
const inviteCode = document.getElementById("invite-code").textContent;
const copyStatus = document.getElementById("copy-status");
const zhButton = document.getElementById("language-zh");
const enButton = document.getElementById("language-en");
let language = navigator.language.toLowerCase().startsWith("zh") ? "zh-CN" : "en";
function setLanguage(nextLanguage) {
language = nextLanguage;
document.documentElement.lang = language;
zhButton.setAttribute("aria-pressed", String(language === "zh-CN"));
enButton.setAttribute("aria-pressed", String(language === "en"));
copyStatus.textContent = "";
}
zhButton.addEventListener("click", () => setLanguage("zh-CN"));
enButton.addEventListener("click", () => setLanguage("en"));
setLanguage(language);
copyButton.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(inviteCode);
copyStatus.textContent = language === "zh-CN" ? "已复制" : "Copied";
} catch {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(document.getElementById("invite-code"));
selection.removeAllRanges();
selection.addRange(range);
copyStatus.textContent = language === "zh-CN"
? "无法自动复制,邀请码已选中"
: "Automatic copy failed; the code is selected";
}
});
</script>
</body>
</html>
+45
View File
@@ -0,0 +1,45 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>{"time":"%date{ISO8601}","level":"%level","logger":"%logger{36}","message":"%replace(%msg){'[\r\n]+',' '}"}%n</pattern>
</encoder>
</appender>
<logger name="io.netty" level="WARN"/>
<logger name="org.jetbrains.exposed" level="WARN"/>
<logger name="com.zaxxer.hikari" level="INFO"/>
<root level="${LOG_LEVEL:-INFO}">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{ISO8601} %-5level [%thread] %logger{24} - %msg%n</pattern>
</encoder>
</appender>
<logger name="io.netty" level="WARN"/>
<logger name="org.jetbrains.exposed" level="WARN"/>
<logger name="com.zaxxer.hikari" level="INFO"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} %-5level [%thread] %logger{36} requestId=%X{requestId:-} - %msg%n</pattern>
</encoder>
</appender>
<logger name="io.netty" level="WARN"/>
<logger name="org.jetbrains.exposed" level="WARN"/>
<logger name="com.zaxxer.hikari" level="INFO"/>
<root level="${LOG_LEVEL:-INFO}">
<appender-ref ref="STDOUT"/>
</root>
</configuration>