I have a site using Netlify Identity and netlify-identity-widget. We’ve found some issues with how it works in Safari, and from what I can see on the repo, it looks like the widget hasn’t been updated recently. We are looking at just building our own UI and using gotrue direct, but was curious if anyone had an example of this they could share.
I have an example, but you might not like it. Setting up Identity without the Identity Widget is a task, especially since the Identity Endpoints are not documented. In the past, I was working on a project to try implementing my own logic and had got successful for the most part, but I haven’t completed it. Here’s the example using Alpine:
import Alpine from 'alpinejs'
import Axios from 'axios'
function onLoad() {
window.removeEventListener('load', onLoad)
Alpine.data('alpine', () => ({
auth: {
axios: Axios.create({
baseURL: 'https://site.netlify.app/.netlify/identity',
headers: {
'x-use-cookie': 1
},
method: 'post'
}),
email: null,
error: null,
loading: false,
login() {
this.auth.loading = true
this.auth.text = 'Logging in...'
this.auth.axios({
data: `grant_type=password&password=${encodeURIComponent(this.auth.password)}&username=${encodeURIComponent(this.auth.email)}`,
url: '/token'
}).then(({data}) => {
this.auth.save(data).then(() => {
this.auth.loading = false
this.auth.text = 'Successfully logged in! Redirecting...'
this.auth.reload()
})
}).catch(({response: {data: {error_description}, status}}) => {
this.auth.error = {
message: `"${error_description}"`,
status
}
this.auth.loading = false
this.auth.text = null
})
},
logout() {
this.auth.loading = true
this.auth.text = 'Logging out...'
this.auth.axios({
headers: {
authorization: `Bearer ${JSON.parse(localStorage.getItem('user')).token.access_token}`
},
url: '/logout'
}).then(() => {
localStorage.removeItem('user')
this.auth.loading = false
this.auth.text = 'Successfully logged out!'
location.href = '/'
}).catch(() => {
this.auth.loading = false
this.auth.text = null
})
},
password: null,
passwordVisible: false,
recover() {
this.auth.loading = true
this.auth.text = 'Sending password reset e-mail...'
this.auth.axios({
data: {
email: this.auth.email
},
url: '/recover'
}).then(() => {
this.auth.loading = false
this.auth.text = 'Password reset e-mail successfully sent!'
}).catch(({response: {data: {msg}, status}}) => {
this.auth.error = {
message: `"${msg}"`,
status
}
this.auth.loading = false
this.auth.text = null
})
},
reload() {
if (location.pathname === '/') {
location.href = '/path/'
} else {
location.reload()
}
},
refresh() {
this.loading = true
this.text = 'Refreshing JWT...'
this.axios({
data: `grant_type=refresh_token&refresh_token=${JSON.parse(localStorage.getItem('user')).token.refresh_token}`,
url: '/token'
}).then(({data}) => {
this.save(data).then(() => {
this.loading = false
this.text = 'JWT successfully refreshed! Redirecting...'
this.reload()
})
}).catch(({response: {data: {error_description}, status}}) => {
this.error = {
message: `"${error_description}"`,
status
}
localStorage.removeItem('user')
this.loading = false
})
},
save(token) {
return new Promise((resolve, reject) => {
this.axios({
headers: {
authorization: `Bearer ${token.access_token}`
},
method: 'get',
url: '/user'
}).then(({data}) => {
localStorage.setItem('user', JSON.stringify({
...data,
token: {
...token,
expires_at: Date.now() + 3000000,
expires_in: 3000
},
url: this.axios.defaults.baseURL
}))
resolve()
}).catch(error => {
reject(error)
})
})
},
signup() {
this.auth.loading = true
this.auth.text = 'Signing up...'
this.auth.axios({
data: {
password: this.auth.password,
token: this.auth.token,
type: 'signup'
},
url: '/verify'
}).then(({data}) => {
this.auth.token = null
this.auth.save(data).then(() => {
this.auth.loading = false
this.auth.text = 'Successfully signed up! Redirecting...'
this.auth.reload()
})
}).catch(({response: {data: {msg}, status}}) => {
this.auth.error = {
message: `"${msg}"`,
status
}
this.auth.token = null
this.auth.loading = false
})
},
update() {
this.auth.loading = true
this.auth.text = 'Updating password...'
this.auth.axios({
data: {
password: this.auth.password
},
headers: {
authorization: `Bearer ${JSON.parse(localStorage.getItem('user')).token.access_token}`
},
method: 'put',
url: '/user'
}).then(() => {
this.auth.token = null
this.auth.loading = false
this.auth.text = 'Password successfully updated! Redirecting...'
this.auth.reload()
}).catch(({response: {data: {msg}, status}}) => {
this.auth.error = {
message: `"${msg}"`,
status
}
this.auth.token = null
this.auth.loading = false
})
},
tab: 'login',
text: null,
token: null
},
init() {
if (location.hash.includes('#invite_token=')) {
this.auth.tab = 'signup'
this.auth.token = location.hash.slice(14)
history.replaceState(null, null, location.origin)
} else if (location.hash.includes('#recovery_token=')) {
this.auth.tab = 'update'
this.auth.token = location.hash.slice(16)
history.replaceState(null, null, location.origin)
this.auth.loading = true
this.auth.text = 'Verifying recovery token...'
this.auth.axios({
data: {
token: this.auth.token,
type: 'recovery'
},
url: '/verify'
}).then(({data}) => {
this.auth.save(data).then(() => {
this.auth.loading = false
this.auth.text = 'Recovery token verified successfully!'
this.auth.tab = 'update'
})
}).catch(({response: {data: {msg}, status}}) => {
this.auth.error = {
message: `"${msg}"`,
status
}
this.auth.loading = false
this.auth.text = null
})
} else if (localStorage.getItem('user')) {
if (Date.now() > JSON.parse(localStorage.getItem('user')).token.expires_at) {
this.auth.refresh()
} else {
location.href = '/some-path/'
}
}
this.$watch('auth.text', newValue => {
if (newValue) {
setTimeout(() => {
this.auth.text = null
}, 2500)
}
})
}
}))
Alpine.start()
}
window.addEventListener('load', onLoad)
I wanted to release this as an example for self-managing Identity stuff, but due to lack of demand, and change of priorities, I never completed it.
I might not be remembering the code very well, but I can answer some questions you might have.