From 1d65ef77ca1179d050c09e49dd003a37ec1ee85e Mon Sep 17 00:00:00 2001 From: kevin olson Date: Thu, 26 Sep 2024 04:22:18 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20hopefully=20working=20apple=20sign-?= =?UTF-8?q?in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/layout/LayoutLogin.vue | 9 +++ app/types/oauth.d.ts | 33 ++++++++ nuxt.config.ts | 6 ++ package.json | 1 + pnpm-lock.yaml | 109 ++++++++++++++++++++++++++ server/api/[...slug].ts | 4 +- server/controllers/oauth.ts | 52 +++++++++++- server/utils/user.ts | 66 +++++++++------- 8 files changed, 249 insertions(+), 31 deletions(-) diff --git a/app/components/layout/LayoutLogin.vue b/app/components/layout/LayoutLogin.vue index f5a23f5..9802b9c 100644 --- a/app/components/layout/LayoutLogin.vue +++ b/app/components/layout/LayoutLogin.vue @@ -17,6 +17,14 @@ const providers = reactive([ color: 'white', click: async () => await navigateTo('/api/oauth/google', { external: true }), }, + { + name: 'apple', + label: 'Apple', + icon: 'i-mdi-apple', + color: 'white', + click: async () => await navigateTo('/api/oauth/redirect/apple', { external: true }), + }, + /* { name: 'x', @@ -56,6 +64,7 @@ const providers = reactive([ icon: 'i-mdi-github', click: async () => await navigateTo('/api/oauth/github', { external: true }), }, + ]) diff --git a/app/types/oauth.d.ts b/app/types/oauth.d.ts index 0a62897..3ca67f7 100644 --- a/app/types/oauth.d.ts +++ b/app/types/oauth.d.ts @@ -14,6 +14,31 @@ export interface GoogleUserInfo { email: string email_verified: boolean } +/* + +apple user { iss: 'https://appleid.apple.com', + aud: 'fume.bio', + exp: 1727422907, + iat: 1727336507, + sub: '000563.30ae9239e35e4d2da5806a88600e772a.0716', + at_hash: 'PcyA1wqULsWWxgiiXSTGnQ', + email: 'acidjazz@gmail.com', + email_verified: true, + auth_time: 1727336507, + nonce_supported: true } + */ +export interface AppleUserInfo { + aud: string + exp: number + iat: number + sub: string + at_hash: string + email: string + email_verified: boolean + auth_time: number + nonce_supported: boolean +} + export interface GithubUserInfo { login: string id: number @@ -50,6 +75,14 @@ export interface GithubUserInfo { updated_at: string } +export interface AppleUserInfo { + name: { + firstName: string + lastName: string + } + email: string +} + export interface MicrosoftUserInfo { id: string displayName: string diff --git a/nuxt.config.ts b/nuxt.config.ts index 309b440..80a65eb 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,5 +1,6 @@ // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ + devtools: { enabled: true }, extends: ['@nuxt/ui-pro'], modules: [ @@ -47,6 +48,11 @@ export default defineNuxtConfig({ maxAge: 60 * 60 * 24 * 365, // 1 year name: 'fumebio-session', }, + apple: { + teamId: '', + keyIdentifier: '', + privateKey: '', + }, oauth: { google: { clientId: '', diff --git a/package.json b/package.json index 78bc0cf..8ceed7a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@fullcalendar/vue3": "^6.1.15", "@nuxt/ui-pro": "^1.4.3", "@prisma/adapter-d1": "^5.20.0", + "apple-signin-auth": "^1.7.6", "date-fns": "^4.1.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a559097..3e6bc1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@prisma/adapter-d1': specifier: ^5.20.0 version: 5.20.0 + apple-signin-auth: + specifier: ^1.7.6 + version: 1.7.6 date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -2191,6 +2194,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + apple-signin-auth@1.7.6: + resolution: {integrity: sha512-edXKmteQRbsaxZRvF1mJpbI5UgTvoTnUrdco8+KLiFvOIh/naEB4BYu0xkWZ9+OeFQup1YyvVf5PlaaNAJHpGg==} + aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} @@ -2220,6 +2226,9 @@ packages: as-table@1.0.55: resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2321,6 +2330,9 @@ packages: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2864,6 +2876,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3827,6 +3842,16 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + keygrip@1.1.0: resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} engines: {node: '>= 0.6'} @@ -3925,18 +3950,36 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} @@ -4322,6 +4365,9 @@ packages: node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + node-rsa@1.1.1: + resolution: {integrity: sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==} + nopt@5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} engines: {node: '>=6'} @@ -5066,6 +5112,9 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + satori-html@0.3.2: resolution: {integrity: sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==} @@ -8515,6 +8564,14 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + apple-signin-auth@1.7.6: + dependencies: + jsonwebtoken: 9.0.2 + node-fetch: 2.7.0 + node-rsa: 1.1.1 + transitivePeerDependencies: + - encoding + aproba@2.0.0: {} archiver-utils@5.0.2: @@ -8552,6 +8609,10 @@ snapshots: dependencies: printable-characters: 1.0.42 + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + assertion-error@2.0.1: {} ast-kit@1.2.0: @@ -8659,6 +8720,8 @@ snapshots: buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -9166,6 +9229,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.28: {} @@ -10369,6 +10436,30 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.3 + + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + keygrip@1.1.0: dependencies: tsscmp: 1.0.6 @@ -10513,14 +10604,26 @@ snapshots: lodash.defaults@4.2.0: {} + lodash.includes@4.3.0: {} + lodash.isarguments@3.1.0: {} + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + lodash.isplainobject@4.0.6: {} + lodash.isstring@4.0.1: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash.uniq@4.5.0: {} lodash@4.17.21: {} @@ -11133,6 +11236,10 @@ snapshots: node-releases@2.0.18: {} + node-rsa@1.1.1: + dependencies: + asn1: 0.2.6 + nopt@5.0.0: dependencies: abbrev: 1.1.1 @@ -12138,6 +12245,8 @@ snapshots: safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} + satori-html@0.3.2: dependencies: ultrahtml: 1.5.3 diff --git a/server/api/[...slug].ts b/server/api/[...slug].ts index 0b24481..94f38ba 100644 --- a/server/api/[...slug].ts +++ b/server/api/[...slug].ts @@ -2,7 +2,7 @@ import type { Round } from '@prisma/client' import { createRouter, useBase } from 'h3' import type { Token, User } from '~/types/models' import logout from '../controllers/logout' -import { githubHandler, googleHandler } from '../controllers/oauth' +import { appleHandler, appleRedirectHandler, githubHandler, googleHandler } from '../controllers/oauth' import round from '../controllers/round' import test from '../controllers/test' import token from '../controllers/token' @@ -24,6 +24,8 @@ if (useRuntimeConfig().appEnv === 'test') router.post('/test/session', test.create) router.get('/oauth/google', googleHandler) +router.get('/oauth/redirect/apple', appleRedirectHandler) +router.post('/oauth/apple', appleHandler) // router.get('/oauth/facebook', facebookHandler) // router.get('/oauth/instagram', instagramHandler) // router.get('/oauth/x', xHandler) diff --git a/server/controllers/oauth.ts b/server/controllers/oauth.ts index 65f3013..da32e4e 100644 --- a/server/controllers/oauth.ts +++ b/server/controllers/oauth.ts @@ -1,9 +1,11 @@ import type { EventHandlerRequest, H3Event } from 'h3' +import appleSignin from 'apple-signin-auth' import type { User } from '~/types/models' -import type { GithubUserInfo, GoogleUserInfo, MicrosoftUserInfo, TokenLocation, UserInfo } from '~/types/oauth' +import type { AppleUserInfo, GithubUserInfo, GoogleUserInfo, MicrosoftUserInfo, TokenLocation, UserInfo } from '~/types/oauth' +import { createSession, userFromEmail } from '../utils/user' const signIn = async (event: H3Event, oauthPayload: any, provider: string): Promise => { - let userPayload: GithubUserInfo | GoogleUserInfo | MicrosoftUserInfo | null = null + let userPayload: GithubUserInfo | GoogleUserInfo | MicrosoftUserInfo | AppleUserInfo | null = null userPayload = oauthPayload as GithubUserInfo const info: UserInfo = { email: '', name: '', avatar: '' } @@ -27,6 +29,12 @@ const signIn = async (event: H3Event, oauthPayload: any, pr info.name = userPayload.displayName info.avatar = '' } + if (provider === 'apple') { + userPayload = oauthPayload as AppleUserInfo + info.email = userPayload.email + info.name = `${userPayload.name.firstName} ${userPayload.name.lastName}` + info.avatar = '' + } if (provider === 'facebook') console.log('facebook payload', oauthPayload) @@ -88,3 +96,43 @@ export const xHandler = oauthXEventHandler({ return sendRedirect(event, '/') }, }) + +export const appleRedirectHandler = defineEventHandler((event) => { + const options = { + clientID: 'fume.bio', + redirectUri: 'https://fume.bio/api/oauth/apple', + scope: 'email name', + } + const authorizationUrl = appleSignin.getAuthorizationUrl(options) + return sendRedirect(event, authorizationUrl) +}) + +export const appleHandler = defineEventHandler(async (event) => { + const body = await readBody(event) + const config = useRuntimeConfig(event) + const clientSecret = appleSignin.getClientSecret({ + clientID: 'fume.bio', + teamID: config.apple.teamId, + privateKey: `-----BEGIN PRIVATE KEY----- +${config.apple.privateKey.split(':BR:').join('\n')} +-----END PRIVATE KEY-----`, + keyIdentifier: config.apple.keyIdentifier, + }) + + const options = { + clientID: 'fume.bio', + redirectUri: 'https://fume.bio/api/oauth/apple', + clientSecret, + scope: 'email name', + } + const tokenResponse = await appleSignin.getAuthorizationToken(body.code, options) + const user = await appleSignin.verifyIdToken(tokenResponse.id_token) + let dbUser + if (body.user) + dbUser = await signIn(event, JSON.parse(body.user), 'apple') + else + dbUser = await createSession('apple', await userFromEmail(user.email), event) + + await setUserSession(event, { user: dbUser }) + return sendRedirect(event, '/') +}) diff --git a/server/utils/user.ts b/server/utils/user.ts index b2a5c8c..412aa6c 100644 --- a/server/utils/user.ts +++ b/server/utils/user.ts @@ -3,6 +3,43 @@ import { usePrisma } from '~/../server/utils/prisma' import type { User } from '~/types/models' import type { TokenLocation, UserInfo } from '~/types/oauth' +export const userFromEmail = async (email: string): Promise => await usePrisma().user.findUnique({ + where: { + email, + }, +}) as User + +export const createSession = async (provider: string, user: User, event?: H3Event) => { + const coordinate = event + ? `${event.node.req.headers['Cloudfront-Viewer-Latitude']} ${event.node.req.headers['Cloudfront-Viewer-Longitude']}` + : '30.2423 -97.7672' + + const location: TokenLocation = { + city: event?.node.req.headers['Cloudfront-Viewer-City'] as string || 'Austin', + region: event?.node.req.headers['Cloudfront-Viewer-Region-Name'] as string || 'TX', + country: event?.node.req.headers['Cloudfront-Viewer-Country'] as string || 'US', + timezone: event?.node.req.headers['Cloudfront-Viewer-Timezone'] as string || 'America/Chicago', + countryName: event?.node.req.headers['Cloudfront-Viewer-CountryName'] as string || 'United States', + } + + const cfg = useRuntimeConfig(event) + + const token = await usePrisma(event).token.create({ + data: { + userId: user.id, + hash: `${cfg.public.prefix}_${generateHash(64)}`, + source: `oauth:${provider}`, + ip: event?.node.req.headers['x-forwarded-for'] as string || '127.0.0.1', + agent: event?.node.req.headers['user-agent'] as string || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', + location: JSON.stringify(location), + coordinate, + }, + }) + + user.hash = token.hash + return user +} + export const createUser = async (info: UserInfo, provider: string, oauthPayload: any, event?: H3Event): Promise => { let user: User | null = null @@ -48,34 +85,7 @@ export const createUser = async (info: UserInfo, provider: string, oauthPayload: }, }) - const coordinate = event - ? `${event.node.req.headers['Cloudfront-Viewer-Latitude']} ${event.node.req.headers['Cloudfront-Viewer-Longitude']}` - : '30.2423 -97.7672' - - const location: TokenLocation = { - city: event?.node.req.headers['Cloudfront-Viewer-City'] as string || 'Austin', - region: event?.node.req.headers['Cloudfront-Viewer-Region-Name'] as string || 'TX', - country: event?.node.req.headers['Cloudfront-Viewer-Country'] as string || 'US', - timezone: event?.node.req.headers['Cloudfront-Viewer-Timezone'] as string || 'America/Chicago', - countryName: event?.node.req.headers['Cloudfront-Viewer-CountryName'] as string || 'United States', - } - - const cfg = useRuntimeConfig(event) - - const token = await usePrisma(event).token.create({ - data: { - userId: user.id, - hash: `${cfg.public.prefix}_${generateHash(64)}`, - source: `oauth:${provider}`, - ip: event?.node.req.headers['x-forwarded-for'] as string || '127.0.0.1', - agent: event?.node.req.headers['user-agent'] as string || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', - location: JSON.stringify(location), - coordinate, - }, - }) - - user.hash = token.hash - return user + return createSession(provider, user, event) } function generateHash(length: number): string {