Skip to content

Commit 646853e

Browse files
authored
Fix case-sensitivity issues around backend unique identifiers (#2964)
* Fix case-sensitivity issues around backend unique identifiers * Fix route param names & username update issue
1 parent 1d3e2ac commit 646853e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+746
-347
lines changed

spx-gui/AGENTS.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@ Keep import statements in order:
3535
- Enum names and enum members
3636
- Vue component names
3737

38+
### Identifier Resolution
39+
40+
When working with backend unique string identifiers such as `username`, project owner, and project name, distinguish unresolved identifiers from canonical identifiers.
41+
42+
* Route params, query params, manual user input, and JWT-derived identifiers are unresolved unless the current task establishes a stronger guarantee.
43+
44+
* Backend-issued values from HTTP API responses are canonical.
45+
46+
* Prefer explicit unresolved names with an `Input` suffix.
47+
48+
* In models and resolved state, names like `owner`, `name`, and `username` should refer to canonical values.
49+
50+
* Keep normal strict and case-sensitive equality (`===` / `!==`) for consuming identifiers. Do not spread ad hoc case-normalization logic across comparison sites.
51+
52+
* Resolve unresolved identifiers at clear boundaries before consuming them. Typical resolution boundaries include project loading, user loading, and other backend-backed fetches.
53+
54+
* Avoid storing unresolved identifiers on long-lived models as if they were already canonical. Prefer passing unresolved identifiers as load or resolve parameters, then writing canonical values onto the model after resolution.
55+
56+
* Downstream logic should consume canonical values for behavior-sensitive checks such as ownership checks, permission checks, project reuse checks, and local-cache decisions.
57+
58+
* Cache keys and similar identity-scoping data may intentionally use unresolved identifiers when that preserves stable session scoping.
59+
3860
## TypeScript Testing
3961

4062
* Use `describe` to group related tests.

spx-gui/src/components/agent-copilot/CopilotProvider.vue

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ import { ToolRegistry } from './mcp/registry'
9595
import { Collector } from './mcp/collector'
9696
import CopilotUI from './CopilotUI.vue'
9797
import { z } from 'zod'
98-
import { getSignedInUsername } from '@/stores/user'
98+
import { untilLoaded } from '@/utils/query'
99+
import { useSignedInStateQuery } from '@/stores/user'
99100
import { createProjectToolDescription, CreateProjectArgsSchema } from './mcp/definitions'
100101
import { getProject, Visibility } from '@/apis/project'
101102
import { useRouter } from 'vue-router'
@@ -114,6 +115,7 @@ const visible = ref(false)
114115
const mcpDebuggerVisible = ref(false)
115116
const showEnvPanel = ref(false)
116117
const router = useRouter()
118+
const signedInStateQuery = useSignedInStateQuery()
117119
118120
const toggleEnvPanel = () => {
119121
showEnvPanel.value = !showEnvPanel.value
@@ -179,18 +181,19 @@ const initBasicTools = async () => {
179181
async function createProject(options: CreateProjectOptions) {
180182
const projectName = options.projectName
181183
182-
// Check if user is signed in
183-
const signedInUsername = getSignedInUsername()
184-
if (signedInUsername == null) {
184+
const signedInState = await untilLoaded(signedInStateQuery)
185+
if (!signedInState.isSignedIn) {
185186
return {
186187
success: false,
187188
message: 'Please sign in to create a project'
188189
}
189190
}
190191
192+
const { username } = signedInState.user
193+
191194
try {
192195
// Check if project already exists
193-
const project = await getProject(signedInUsername, options.projectName)
196+
const project = await getProject(username, options.projectName)
194197
if (project != null) {
195198
return {
196199
success: false,
@@ -201,7 +204,7 @@ async function createProject(options: CreateProjectOptions) {
201204
// Handle error checking project existence
202205
}
203206
204-
const project = new SpxProject(signedInUsername, projectName)
207+
const project = new SpxProject(username, projectName)
205208
project.setVisibility(Visibility.Private)
206209
207210
try {

spx-gui/src/components/agent-copilot/UserMessage.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const props = defineProps<{
77
content: string
88
}>()
99
10-
const { data: signedInUser } = useSignedInUser()
10+
const signedInUser = useSignedInUser()
1111
const avatarUrl = useAvatarUrl(() => signedInUser.value?.avatar)
1212
</script>
1313

spx-gui/src/components/asset/gen/sprite/SpriteGen.vue

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
<script setup lang="ts">
2-
import { computed } from 'vue'
32
import { computedShallowReactive, useComputedDisposable } from '@/utils/utils'
43
import { useI18n } from '@/utils/i18n'
54
import { useNetwork } from '@/utils/network'
65
import type { Sprite } from '@/models/spx/sprite'
76
import type { SpriteGen } from '@/models/spx/gen/sprite-gen'
8-
import { getSignedInUsername } from '@/stores/user'
7+
import { useSignedInStateQuery } from '@/stores/user'
98
import { cloudHelpers } from '@/models/common/cloud'
109
import { provideLocalEditorCtx } from '@/components/editor/EditorContextProvider.vue'
1110
import { EditorState } from '@/components/editor/editor-state'
@@ -25,7 +24,7 @@ const emit = defineEmits<{
2524
}>()
2625
2726
const i18n = useI18n()
28-
const signedInUsername = computed(() => getSignedInUsername())
27+
const signedInStateQuery = useSignedInStateQuery()
2928
const { isOnline } = useNetwork()
3029
// Local cache is not really used in sprite gen, so a dummy implementation is sufficient.
3130
const localCache: ILocalCache = {
@@ -35,7 +34,7 @@ const localCache: ILocalCache = {
3534
}
3635
// We should override the state to avoid history operations caused by animation / costume changes within sprite gen.
3736
const editorStateInGen = useComputedDisposable(
38-
() => new EditorState(i18n, props.gen.previewProject, isOnline, signedInUsername.value, cloudHelpers, localCache)
37+
() => new EditorState(i18n, props.gen.previewProject, isOnline, signedInStateQuery, cloudHelpers, localCache)
3938
)
4039
const editorCtxInGen = computedShallowReactive(() => ({
4140
project: props.gen.previewProject,

spx-gui/src/components/asset/library/AssetSaveModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const emit = defineEmits<{
3939
resolved: []
4040
}>()
4141
42-
const { data: signedInUser } = useSignedInUser()
42+
const signedInUser = useSignedInUser()
4343
4444
const user = computed(() => {
4545
const u = signedInUser.value

spx-gui/src/components/common/ListResultWrapper.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { UILoading, UIError, UIEmpty } from '@/components/ui'
1313
import type { ByPage } from '@/apis/common'
1414
1515
const props = defineProps<{
16-
queryRet: QueryRet<T>
16+
queryRet: QueryRet<T | null>
1717
height?: number
1818
/** Type of the list content */
1919
contentType?: 'project'

spx-gui/src/components/community/ProjectsSection.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type Context = 'home' | 'user' | 'project'
4646
4747
defineProps<{
4848
linkTo?: string | null
49-
queryRet: QueryRet<unknown[]>
49+
queryRet: QueryRet<unknown[] | null>
5050
context: Context
5151
numInRow: number
5252
}>()

spx-gui/src/components/community/project/ReleaseHistory.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { UITimeline, UITimelineItem, UILoading, UIError } from '@/components/ui'
66
import TextView from '../TextView.vue'
77
88
defineProps<{
9-
queryRet: QueryRet<ProjectRelease[]>
9+
queryRet: QueryRet<ProjectRelease[] | null>
1010
}>()
1111
</script>
1212

spx-gui/src/components/community/user/FollowButton.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import { computed } from 'vue'
3-
import { getSignedInUsername } from '@/stores/user'
3+
import { useSignedInUser } from '@/stores/user'
44
import { UIButton } from '@/components/ui'
55
import { useMessageHandle } from '@/utils/exception'
66
import { useEnsureSignedIn } from '@/utils/user'
@@ -11,8 +11,9 @@ const props = defineProps<{
1111
name: string
1212
}>()
1313
14+
const signedInUser = useSignedInUser()
1415
const followable = computed(() => {
15-
const signedInUsername = getSignedInUsername()
16+
const signedInUsername = signedInUser.value?.username
1617
return signedInUsername != null && props.name !== signedInUsername
1718
})
1819

spx-gui/src/components/community/user/UserHeader.vue

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,63 @@ import { useRoute, useRouter } from 'vue-router'
44
import { type User } from '@/apis/user'
55
import { useAvatarUrl } from '@/stores/user/avatar'
66
import { useMessageHandle } from '@/utils/exception'
7-
import { getSignedInUsername } from '@/stores/user'
8-
import { UIButton, UIImg, useModal } from '@/components/ui'
7+
import { useI18n } from '@/utils/i18n'
8+
import { timeout } from '@/utils/utils'
9+
import { initiateSignIn, useSignedInUser } from '@/stores/user'
10+
import { UIButton, UIImg, useMessage, useModal } from '@/components/ui'
911
import CommunityCard from '@/components/community/CommunityCard.vue'
1012
import TextView from '../TextView.vue'
1113
import FollowButton from './FollowButton.vue'
1214
import UserJoinedAt from './UserJoinedAt.vue'
1315
import EditProfileModal from './EditProfileModal.vue'
1416
import UserUsernameInline from './UserUsernameInline.vue'
1517
import { getCoverImgUrl } from './cover'
16-
import { getUserPageRoute } from '@/router'
1718
1819
const props = defineProps<{
1920
user: User
2021
}>()
2122
2223
const router = useRouter()
2324
const route = useRoute()
24-
const isSignedInUser = computed(() => props.user.username === getSignedInUsername())
25+
const signedInUser = useSignedInUser()
26+
const isSignedInUser = computed(() => props.user.username === signedInUser.value?.username)
2527
const avatarUrl = useAvatarUrl(() => props.user.avatar)
2628
const coverImgUrl = computed(() => getCoverImgUrl(props.user.username))
2729
2830
const invokeEditProfileModal = useModal(EditProfileModal)
2931
30-
function replaceCurrentUserRoute(oldUsername: string, newUsername: string) {
31-
if (oldUsername === newUsername) return
32-
if (route.params.name !== oldUsername) return
33-
34-
const oldBasePath = getUserPageRoute(oldUsername)
35-
if (!route.path.startsWith(oldBasePath)) return
36-
37-
router.replace({
38-
path: `${getUserPageRoute(newUsername)}${route.path.slice(oldBasePath.length)}`,
39-
query: route.query,
40-
hash: route.hash
41-
})
42-
}
32+
const i18n = useI18n()
33+
const message = useMessage()
34+
35+
const handleUsernameUpdated = useMessageHandle(
36+
async (newUsername: string) => {
37+
await router.replace({
38+
params: {
39+
nameInput: newUsername
40+
},
41+
query: route.query,
42+
hash: route.hash
43+
})
44+
message.success(
45+
i18n.t({
46+
en: 'Username updated successfully. Redirecting to the sign-in page...',
47+
zh: '用户名更新成功。正在重定向到登录页面...'
48+
})
49+
)
50+
await timeout(2000)
51+
initiateSignIn()
52+
},
53+
{
54+
en: 'Failed to redirect after username update',
55+
zh: '用户名更新后重定向失败'
56+
}
57+
).fn
4358
4459
const handleEditProfile = useMessageHandle(
4560
async () => {
4661
const oldUsername = props.user.username
4762
const updated = await invokeEditProfileModal({ user: props.user })
48-
replaceCurrentUserRoute(oldUsername, updated.username)
63+
if (oldUsername !== updated.username) return handleUsernameUpdated(updated.username)
4964
},
5065
{
5166
en: 'Failed to update profile',

0 commit comments

Comments
 (0)