I cannot understand why when my stencil.js app loads, it runs the componentWillLoad() lifecycle method twice, and just before the second one forces the url to lowercase. This is a problem as the url contains an encoded UID so when it is forced to lowercase and then I extract the encoded uid, I can no longer decode in the backend.
The full story:
I am working on an account activation feature.
- My django backend uses Djoser, Sendgrid and Anymail to send an email activation link to a user after their account is created. The uid of the user is encoded using
djoser.utils.encode_uid(uid)
and added to the url - The user receives the email and clicks a button which takes them to a page on my stencil app where they (as an anonymous user) enter their password and name.
- When the form is submitted, the token from the url is used by the backend to authenticate the activation of the user's account. The user to be activated is determined by the uid from the url once the backend has decoded it using `djoser.utils.decode_uid(uid).
The problem:
After opening the stencil page from the email link (baseUrl/users/{uid}/activate/{token}/, for a few milliseconds, the URL is perfect for my needs as the encoded uid is exactly as it was encoded by my django app (mixed case). At this point, my componentWillLoad() lifecycle method extracts the encoded uid and the token from the url and stores it in the component state. However, after literally a few milliseconds, the page reloads and the url has the same characters but is now lowercase. This is an issue as the encoded UID has now changed (djoser encoding is case sensitive). The componentWillLoad() runs again, extracts the now incorrect encoded UID, and submits it in the form and so the django backend fails to decode it and identify the user to activate.
Here is a partial of my stencil component:
@Component({
tag: 'app-activate',
styleUrl: 'app-activate.css',
})
export class AppActivate {
@State() name: string
@State() new_password: string
@State() re_new_password: string
@State() email: string
@State() uid: string
@State() token: string
api: Auricle
constructor() {
this.api = new Auricle()
}
getDataFromUrl(newUrl: string) {
console.log('fetching values')
const splitOne = newUrl.split('users/')[1]
const uid = splitOne.split('activate/')[0].slice(0, -1)
const token = splitOne.split('activate/')[1]
console.log('SPLIT RESULT: ', { uid: uid, token: token, orginal_url: window.location.href })
const encodedUid = uid
const decodedUid = atob(encodedUid)
console.log('test', decodedUid) // Output: "1234567890"
return { uid, token }
}
componentWillLoad() {
const values = this.getDataFromUrl(Router.url.pathname)
this.uid = values['uid']
this.token = values.token
}
render() {
return (
///
)
}
}
///
Here is the package.json dependencies for my stencil app:
"devDependencies": {
"@fullhuman/postcss-purgecss": "^4.0.3",
"@reduxjs/toolkit": "1.9.1",
"@rollup/plugin-node-resolve": "^13.3.0",
"@stencil/core": "npm:@johnjenkins/stencil-community@2.4.0",
"@stencil/postcss": "^2.0.0",
"@stencil/router": "^1.0.1",
"@types/css-font-loading-module": "0.0.4",
"@types/node": "^14.14.41",
"autoprefixer": "^10.2.5",
"cross-env": "^7.0.3",
"cssnano": "^5.0.1",
"postcss-import": "^14.0.2",
"postcss-replace": "^1.1.3",
"release-it": "*",
"rollup": "^2.50.5",
"rollup-plugin-dotenv": "^0.4.0",
"rollup-plugin-node-polyfills": "^0.2.1",
"rollup-plugin-node-resolve": "^5.2.0",
"tailwindcss": "^2.1.1",
"typescript": "^2.9.2"
},
"license": "MIT",
"dependencies": {
"@codexteam/shortcuts": "^1.1.1",
"@editorjs/editorjs": "^2.22.2",
"@ionic/core": "^5.6.6",
"@ionic/storage": "^3.0.4",
"@types/jest": "^26.0.22",
"custom-avatar-initials": "^1.0.1",
"fuse.js": "^6.4.6",
"jwt-check-expiration": "^1.0.5",
"localforage-driver-memory": "^1.0.5",
"lodash.sortby": "^4.7.0",
"lodash.truncate": "^4.4.2",
"moment": "^2.29.1",
"stencil-router-v2": "^0.6.0"
}
To reiterate, the flow I need is:
- User creates account
- User receives activation email
- User follows link (which includes uid & token)
- URL path variables are extracted without the url reloading in lowercase.
I know that the encoding is correct as for a split second it appears correctly in the browser. If I log it the first time componentWillLoad() runs, and then copy the encoded UID and decode it, it gives me the correct UID. So the issue is definitely that for some reason, sometime after the first componentWillLoad() (which should only run once) the url is forced to lowercase and the lifecycle methods start again.
Ideally, I would like to ensure that the url does not change at all, but failing that, can anyone think of any workarounds to store the encoded UID from the url on the first componentWillLoad() run, and then use that initial value in the form submission?
thanks in advance
UPDATE
I have come up with one workaround, which I will share in case it is useful for others however I must disclaim that I dislike it for two reasons:
- Primarily, I find the initial behaviour of the url formatting to lowercase inexplicable and feel frustrated that I have not found any documentation that explain why this happens or how to prevent it. I assume the stencil-router-v2 is responsible for the behaviour and probably has some built-in normalising hook for urls (perhaps for SEO reasons). I would be very grateful for any more informed explanation.
- My solution uses localstorage, which while effective, feels more hacky than necessary for the task at hand. There may even be some security risks to this, given that the uid and activation token are being stored.
My workaround:
correctUrlWorkAround(uid: string, token: string) {
const storedUID = localStorage.getItem('activation_uid')
const storedToken = localStorage.getItem('activation_token')
if (storedUID) {
this.activationData['uid'] = storedUID
} else {
// Store the value in localStorage
localStorage.setItem('activation_uid', uid)
this.activationData['uid'] = localStorage.getItem('activation_uid')
}
if (storedToken) {
this.activationData['token'] = storedToken
} else {
// Store the value in localStorage
localStorage.setItem('activation_token', token)
this.activationData['token'] = localStorage.getItem('activation_token')
}
}