@jskit-ai/users-core 0.1.20 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.descriptor.mjs +11 -2
- package/package.json +2 -2
- package/src/server/common/repositories/workspacesRepository.js +1 -1
- package/src/shared/support/usersApiPaths.js +11 -2
- package/templates/migrations/users_core_initial.cjs +0 -1
- package/templates/migrations/users_core_workspace_settings_single_name_source.cjs +54 -0
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +0 -20
- package/test/usersApiPaths.test.js +18 -0
- package/test/usersRouteRequestInputValidator.test.js +2 -2
- package/test/workspaceSettingsRepository.test.js +1 -12
- package/test/workspaceSettingsResource.test.js +1 -16
- package/test/workspaceSettingsService.test.js +0 -6
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/users-core",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.22",
|
|
5
5
|
description: "Users/workspace domain runtime plus HTTP routes for workspace, account, and console features.",
|
|
6
6
|
dependsOn: [
|
|
7
7
|
"@jskit-ai/auth-core",
|
|
@@ -206,7 +206,7 @@ export default Object.freeze({
|
|
|
206
206
|
"@jskit-ai/auth-core": "0.1.15",
|
|
207
207
|
"@jskit-ai/database-runtime": "0.1.16",
|
|
208
208
|
"@jskit-ai/http-runtime": "0.1.15",
|
|
209
|
-
"@jskit-ai/kernel": "0.1.
|
|
209
|
+
"@jskit-ai/kernel": "0.1.16",
|
|
210
210
|
"@fastify/multipart": "^9.4.0",
|
|
211
211
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
212
212
|
"typebox": "^1.0.81"
|
|
@@ -252,6 +252,15 @@ export default Object.freeze({
|
|
|
252
252
|
category: "migration",
|
|
253
253
|
id: "users-core-console-owner-schema"
|
|
254
254
|
},
|
|
255
|
+
{
|
|
256
|
+
op: "install-migration",
|
|
257
|
+
from: "templates/migrations/users_core_workspace_settings_single_name_source.cjs",
|
|
258
|
+
toDir: "migrations",
|
|
259
|
+
extension: ".cjs",
|
|
260
|
+
reason: "Remove workspace_settings.name so workspace names come from workspaces only.",
|
|
261
|
+
category: "migration",
|
|
262
|
+
id: "users-core-workspace-settings-single-name-source"
|
|
263
|
+
},
|
|
255
264
|
{
|
|
256
265
|
from: "templates/packages/main/src/shared/resources/workspaceSettingsFields.js",
|
|
257
266
|
to: "packages/main/src/shared/resources/workspaceSettingsFields.js",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/users-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@jskit-ai/auth-core": "0.1.15",
|
|
28
28
|
"@jskit-ai/database-runtime": "0.1.16",
|
|
29
29
|
"@jskit-ai/http-runtime": "0.1.15",
|
|
30
|
-
"@jskit-ai/kernel": "0.1.
|
|
30
|
+
"@jskit-ai/kernel": "0.1.16",
|
|
31
31
|
"@fastify/multipart": "^9.4.0",
|
|
32
32
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
33
33
|
"typebox": "^1.0.81"
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { normalizePathname } from "@jskit-ai/kernel/shared/surface/paths";
|
|
2
|
+
import { splitPathQueryAndHash } from "@jskit-ai/kernel/shared/support";
|
|
2
3
|
|
|
3
4
|
const USERS_PUBLIC_API_BASE_PATH = "/api";
|
|
4
5
|
const USERS_WORKSPACE_API_BASE_PATH = "/api/w/:workspaceSlug/workspace";
|
|
5
6
|
|
|
6
7
|
function normalizeApiRelativePath(relativePath = "/") {
|
|
7
|
-
const
|
|
8
|
-
|
|
8
|
+
const { pathname, queryString, hash } = splitPathQueryAndHash(relativePath);
|
|
9
|
+
const normalizedPath = normalizePathname(pathname || "/") || "/";
|
|
10
|
+
const normalizedQueryString = String(queryString || "").trim().replace(/^\?+/, "");
|
|
11
|
+
const normalizedHash = String(hash || "").trim();
|
|
12
|
+
const querySuffix = normalizedQueryString ? `?${normalizedQueryString}` : "";
|
|
13
|
+
return `${normalizedPath}${querySuffix}${normalizedHash}`;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
function normalizeSurfaceWorkspaceRequirement(value = false) {
|
|
@@ -22,6 +27,10 @@ function resolveApiBasePath({ surfaceRequiresWorkspace = false, relativePath = "
|
|
|
22
27
|
return basePath;
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
if (normalizedRelativePath.startsWith("/?") || normalizedRelativePath.startsWith("/#")) {
|
|
31
|
+
return `${basePath}${normalizedRelativePath.slice(1)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
return `${basePath}${normalizedRelativePath}`;
|
|
26
35
|
}
|
|
27
36
|
|
|
@@ -44,7 +44,6 @@ exports.up = async function up(knex) {
|
|
|
44
44
|
|
|
45
45
|
await knex.schema.createTable("workspace_settings", (table) => {
|
|
46
46
|
table.integer("workspace_id").unsigned().primary().references("id").inTable("workspaces").onDelete("CASCADE");
|
|
47
|
-
table.string("name", 160).notNullable().defaultTo("Workspace");
|
|
48
47
|
table.string("avatar_url", 512).notNullable().defaultTo("");
|
|
49
48
|
table.string("light_primary_color", 7).notNullable().defaultTo("#1867C0");
|
|
50
49
|
table.string("light_secondary_color", 7).notNullable().defaultTo("#48A9A6");
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const WORKSPACE_SETTINGS_TABLE = "workspace_settings";
|
|
2
|
+
const WORKSPACES_TABLE = "workspaces";
|
|
3
|
+
const LEGACY_NAME_COLUMN = "name";
|
|
4
|
+
|
|
5
|
+
async function hasTable(knex, tableName) {
|
|
6
|
+
return knex.schema.hasTable(tableName);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function hasColumn(knex, tableName, columnName) {
|
|
10
|
+
return knex.schema.hasColumn(tableName, columnName);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
exports.up = async function up(knex) {
|
|
14
|
+
const hasWorkspaceSettings = await hasTable(knex, WORKSPACE_SETTINGS_TABLE);
|
|
15
|
+
if (!hasWorkspaceSettings) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const hasLegacyName = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_NAME_COLUMN);
|
|
20
|
+
if (!hasLegacyName) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await knex.schema.alterTable(WORKSPACE_SETTINGS_TABLE, (table) => {
|
|
25
|
+
table.dropColumn(LEGACY_NAME_COLUMN);
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
exports.down = async function down(knex) {
|
|
30
|
+
const hasWorkspaceSettings = await hasTable(knex, WORKSPACE_SETTINGS_TABLE);
|
|
31
|
+
if (!hasWorkspaceSettings) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const hasLegacyName = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_NAME_COLUMN);
|
|
36
|
+
if (!hasLegacyName) {
|
|
37
|
+
await knex.schema.alterTable(WORKSPACE_SETTINGS_TABLE, (table) => {
|
|
38
|
+
table.string(LEGACY_NAME_COLUMN, 160).notNullable().defaultTo("Workspace");
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasWorkspaces = await hasTable(knex, WORKSPACES_TABLE);
|
|
43
|
+
if (!hasWorkspaces) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const workspaceRows = await knex(WORKSPACES_TABLE).select("id", "name");
|
|
48
|
+
for (const workspaceRow of workspaceRows) {
|
|
49
|
+
const normalizedName = String(workspaceRow?.name || "").trim() || "Workspace";
|
|
50
|
+
await knex(WORKSPACE_SETTINGS_TABLE)
|
|
51
|
+
.where({ workspace_id: Number(workspaceRow.id) })
|
|
52
|
+
.update({ name: normalizedName });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -35,26 +35,6 @@ function normalizeHexColor(value) {
|
|
|
35
35
|
|
|
36
36
|
resetWorkspaceSettingsFields();
|
|
37
37
|
|
|
38
|
-
defineField({
|
|
39
|
-
key: "name",
|
|
40
|
-
dbColumn: "name",
|
|
41
|
-
required: true,
|
|
42
|
-
inputSchema: Type.String({
|
|
43
|
-
minLength: 1,
|
|
44
|
-
maxLength: 160,
|
|
45
|
-
messages: {
|
|
46
|
-
required: "Workspace name is required.",
|
|
47
|
-
minLength: "Workspace name is required.",
|
|
48
|
-
maxLength: "Workspace name must be at most 160 characters.",
|
|
49
|
-
default: "Workspace name is required."
|
|
50
|
-
}
|
|
51
|
-
}),
|
|
52
|
-
outputSchema: Type.String({ minLength: 1, maxLength: 160 }),
|
|
53
|
-
normalizeInput: (value) => normalizeText(value),
|
|
54
|
-
normalizeOutput: (value) => normalizeText(value),
|
|
55
|
-
resolveDefault: ({ workspace = {} } = {}) => normalizeText(workspace.name) || "Workspace"
|
|
56
|
-
});
|
|
57
|
-
|
|
58
38
|
defineField({
|
|
59
39
|
key: "avatarUrl",
|
|
60
40
|
dbColumn: "avatar_url",
|
|
@@ -29,3 +29,21 @@ test("resolveApiBasePath resolves workspace and non-workspace API base paths", (
|
|
|
29
29
|
"/api/customers"
|
|
30
30
|
);
|
|
31
31
|
});
|
|
32
|
+
|
|
33
|
+
test("resolveApiBasePath preserves query strings and hash fragments", () => {
|
|
34
|
+
assert.equal(
|
|
35
|
+
resolveApiBasePath({
|
|
36
|
+
surfaceRequiresWorkspace: true,
|
|
37
|
+
relativePath: "/customers?search=buddy#top"
|
|
38
|
+
}),
|
|
39
|
+
"/api/w/:workspaceSlug/workspace/customers?search=buddy#top"
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
assert.equal(
|
|
43
|
+
resolveApiBasePath({
|
|
44
|
+
surfaceRequiresWorkspace: false,
|
|
45
|
+
relativePath: "/?cursor=2"
|
|
46
|
+
}),
|
|
47
|
+
"/api?cursor=2"
|
|
48
|
+
);
|
|
49
|
+
});
|
|
@@ -404,7 +404,7 @@ test("workspace settings route handlers build action input from request.input",
|
|
|
404
404
|
createActionRequest({
|
|
405
405
|
input: {
|
|
406
406
|
params: { workspaceSlug: "acme" },
|
|
407
|
-
body: {
|
|
407
|
+
body: { avatarUrl: "https://example.com/acme.png" }
|
|
408
408
|
},
|
|
409
409
|
executeAction
|
|
410
410
|
}),
|
|
@@ -413,7 +413,7 @@ test("workspace settings route handlers build action input from request.input",
|
|
|
413
413
|
|
|
414
414
|
assert.deepEqual(calls[0], {
|
|
415
415
|
actionId: "workspace.settings.update",
|
|
416
|
-
input: { workspaceSlug: "acme", patch: {
|
|
416
|
+
input: { workspaceSlug: "acme", patch: { avatarUrl: "https://example.com/acme.png" } }
|
|
417
417
|
});
|
|
418
418
|
});
|
|
419
419
|
|
|
@@ -16,7 +16,6 @@ function createKnexStub(rowOverrides = {}) {
|
|
|
16
16
|
updatePayload: null,
|
|
17
17
|
row: {
|
|
18
18
|
workspace_id: 1,
|
|
19
|
-
name: "Workspace",
|
|
20
19
|
avatar_url: "",
|
|
21
20
|
light_primary_color: DEFAULT_WORKSPACE_THEME.light.color,
|
|
22
21
|
light_secondary_color: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
|
|
@@ -41,7 +40,6 @@ function createKnexStub(rowOverrides = {}) {
|
|
|
41
40
|
state.insertedRow = { ...payload };
|
|
42
41
|
state.row = {
|
|
43
42
|
workspace_id: payload.workspace_id,
|
|
44
|
-
name: payload.name,
|
|
45
43
|
avatar_url: payload.avatar_url,
|
|
46
44
|
light_primary_color: payload.light_primary_color,
|
|
47
45
|
light_secondary_color: payload.light_secondary_color,
|
|
@@ -69,9 +67,6 @@ function createKnexStub(rowOverrides = {}) {
|
|
|
69
67
|
if (Object.hasOwn(payload, "invites_enabled")) {
|
|
70
68
|
state.row.invites_enabled = payload.invites_enabled;
|
|
71
69
|
}
|
|
72
|
-
if (Object.hasOwn(payload, "name")) {
|
|
73
|
-
state.row.name = payload.name;
|
|
74
|
-
}
|
|
75
70
|
if (Object.hasOwn(payload, "avatar_url")) {
|
|
76
71
|
state.row.avatar_url = payload.avatar_url;
|
|
77
72
|
}
|
|
@@ -122,7 +117,6 @@ test("workspaceSettingsRepository.findByWorkspaceId maps the stored row", async
|
|
|
122
117
|
|
|
123
118
|
assert.deepEqual(record, {
|
|
124
119
|
workspaceId: 1,
|
|
125
|
-
name: "Workspace",
|
|
126
120
|
avatarUrl: "",
|
|
127
121
|
lightPrimaryColor: DEFAULT_WORKSPACE_THEME.light.color,
|
|
128
122
|
lightSecondaryColor: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
|
|
@@ -162,7 +156,6 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
|
|
|
162
156
|
const record = await repository.ensureForWorkspaceId(5);
|
|
163
157
|
|
|
164
158
|
assert.equal(state.insertedRow.workspace_id, 5);
|
|
165
|
-
assert.equal(state.insertedRow.name, "Workspace");
|
|
166
159
|
assert.equal(state.insertedRow.avatar_url, "");
|
|
167
160
|
assert.equal(state.insertedRow.light_primary_color, DEFAULT_WORKSPACE_THEME.light.color);
|
|
168
161
|
assert.equal(state.insertedRow.light_secondary_color, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
|
|
@@ -179,7 +172,6 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
|
|
|
179
172
|
DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor
|
|
180
173
|
);
|
|
181
174
|
assert.equal(state.insertedRow.invites_enabled, false);
|
|
182
|
-
assert.equal(record.name, "Workspace");
|
|
183
175
|
assert.equal(record.avatarUrl, "");
|
|
184
176
|
assert.equal(record.lightPrimaryColor, DEFAULT_WORKSPACE_THEME.light.color);
|
|
185
177
|
assert.equal(record.lightSecondaryColor, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
|
|
@@ -192,22 +184,19 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
|
|
|
192
184
|
assert.equal(record.invitesEnabled, false);
|
|
193
185
|
});
|
|
194
186
|
|
|
195
|
-
test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates
|
|
187
|
+
test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates avatar/color columns", async () => {
|
|
196
188
|
const { knexStub, state } = createKnexStub();
|
|
197
189
|
const repository = createRepository(knexStub, {
|
|
198
190
|
defaultInvitesEnabled: true
|
|
199
191
|
});
|
|
200
192
|
|
|
201
193
|
const updated = await repository.updateSettingsByWorkspaceId(1, {
|
|
202
|
-
name: "New name",
|
|
203
194
|
avatarUrl: "https://example.com/avatar.png",
|
|
204
195
|
lightPrimaryColor: "#123abc"
|
|
205
196
|
});
|
|
206
197
|
|
|
207
|
-
assert.equal(state.updatePayload.name, "New name");
|
|
208
198
|
assert.equal(state.updatePayload.avatar_url, "https://example.com/avatar.png");
|
|
209
199
|
assert.equal(state.updatePayload.light_primary_color, "#123ABC");
|
|
210
|
-
assert.equal(updated.name, "New name");
|
|
211
200
|
assert.equal(updated.avatarUrl, "https://example.com/avatar.png");
|
|
212
201
|
assert.equal(updated.lightPrimaryColor, "#123ABC");
|
|
213
202
|
});
|
|
@@ -46,7 +46,6 @@ function parseBody(operation, payload = {}) {
|
|
|
46
46
|
|
|
47
47
|
test("workspace settings patch body normalizes valid payload before validation", () => {
|
|
48
48
|
const parsed = parseBody(workspaceSettingsResource.operations.patch, {
|
|
49
|
-
name: " Team Mercury ",
|
|
50
49
|
avatarUrl: "https://example.com/avatar.png",
|
|
51
50
|
lightPrimaryColor: "#0f6b54",
|
|
52
51
|
lightSecondaryColor: "#0b4d3c",
|
|
@@ -62,7 +61,6 @@ test("workspace settings patch body normalizes valid payload before validation",
|
|
|
62
61
|
assert.equal(parsed.ok, true);
|
|
63
62
|
assert.deepEqual(parsed.fieldErrors, {});
|
|
64
63
|
assert.deepEqual(parsed.value, {
|
|
65
|
-
name: "Team Mercury",
|
|
66
64
|
avatarUrl: "https://example.com/avatar.png",
|
|
67
65
|
lightPrimaryColor: "#0F6B54",
|
|
68
66
|
lightSecondaryColor: "#0B4D3C",
|
|
@@ -88,19 +86,8 @@ test("workspace settings patch body validates avatar URL protocol", () => {
|
|
|
88
86
|
);
|
|
89
87
|
});
|
|
90
88
|
|
|
91
|
-
test("workspace settings patch body keeps max-length name rule", () => {
|
|
92
|
-
const parsed = parseBody(workspaceSettingsResource.operations.patch, {
|
|
93
|
-
name: "x".repeat(161)
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
assert.equal(parsed.ok, false);
|
|
97
|
-
assert.equal(parsed.fieldErrors.name, "Workspace name must be at most 160 characters.");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
89
|
test("workspace settings create body requires full-write fields", () => {
|
|
101
|
-
const parsed = parseBody(workspaceSettingsResource.operations.create, {
|
|
102
|
-
name: "Mercury Workspace"
|
|
103
|
-
});
|
|
90
|
+
const parsed = parseBody(workspaceSettingsResource.operations.create, {});
|
|
104
91
|
|
|
105
92
|
assert.equal(parsed.ok, false);
|
|
106
93
|
assert.equal(parsed.fieldErrors.lightPrimaryColor, "Light primary color is required.");
|
|
@@ -125,7 +112,6 @@ test("workspace settings output normalizes raw service payloads", () => {
|
|
|
125
112
|
ownerUserId: "9"
|
|
126
113
|
},
|
|
127
114
|
settings: {
|
|
128
|
-
name: " Mercury Workspace ",
|
|
129
115
|
avatarUrl: " https://example.com/avatar.png ",
|
|
130
116
|
lightPrimaryColor: "#0f6b54",
|
|
131
117
|
invitesEnabled: false
|
|
@@ -140,7 +126,6 @@ test("workspace settings output normalizes raw service payloads", () => {
|
|
|
140
126
|
ownerUserId: 9
|
|
141
127
|
},
|
|
142
128
|
settings: {
|
|
143
|
-
name: "Mercury Workspace",
|
|
144
129
|
avatarUrl: "https://example.com/avatar.png",
|
|
145
130
|
lightPrimaryColor: "#0F6B54",
|
|
146
131
|
lightSecondaryColor: expectedTheme.light.secondaryColor,
|
|
@@ -30,7 +30,6 @@ function createFixture({ workspaceInvitationsEnabled = true } = {}) {
|
|
|
30
30
|
color: defaultTheme.light.color
|
|
31
31
|
},
|
|
32
32
|
settings: {
|
|
33
|
-
name: "TonyMobily3",
|
|
34
33
|
avatarUrl: "",
|
|
35
34
|
lightPrimaryColor: defaultTheme.light.color,
|
|
36
35
|
lightSecondaryColor: defaultTheme.light.secondaryColor,
|
|
@@ -75,7 +74,6 @@ test("workspaceSettingsService.getWorkspaceSettings returns the stored invitesEn
|
|
|
75
74
|
);
|
|
76
75
|
|
|
77
76
|
assert.deepEqual(response.settings, {
|
|
78
|
-
name: "TonyMobily3",
|
|
79
77
|
avatarUrl: "",
|
|
80
78
|
lightPrimaryColor: "#0F6B54",
|
|
81
79
|
lightSecondaryColor: "#48A9A6",
|
|
@@ -97,18 +95,15 @@ test("workspaceSettingsService.updateWorkspaceSettings writes editable fields th
|
|
|
97
95
|
const response = await service.updateWorkspaceSettings(
|
|
98
96
|
state.workspace,
|
|
99
97
|
{
|
|
100
|
-
name: "New Name",
|
|
101
98
|
invitesEnabled: false
|
|
102
99
|
},
|
|
103
100
|
authorizedOptions(["workspace.settings.update"])
|
|
104
101
|
);
|
|
105
102
|
|
|
106
103
|
assert.deepEqual(state.settingsPatch, {
|
|
107
|
-
name: "New Name",
|
|
108
104
|
invitesEnabled: false
|
|
109
105
|
});
|
|
110
106
|
assert.deepEqual(response.settings, {
|
|
111
|
-
name: "New Name",
|
|
112
107
|
avatarUrl: "",
|
|
113
108
|
lightPrimaryColor: "#0F6B54",
|
|
114
109
|
lightSecondaryColor: "#48A9A6",
|
|
@@ -135,7 +130,6 @@ test("workspaceSettingsService disables invite settings in output when app polic
|
|
|
135
130
|
);
|
|
136
131
|
|
|
137
132
|
assert.deepEqual(response.settings, {
|
|
138
|
-
name: "TonyMobily3",
|
|
139
133
|
avatarUrl: "",
|
|
140
134
|
lightPrimaryColor: "#0F6B54",
|
|
141
135
|
lightSecondaryColor: "#48A9A6",
|