diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 59c237b6..137c6eb1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,13 +16,10 @@ // Add the IDs of extensions you want installed when the container is created. "extensions": [ "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", "Vue.volar", - "nrwl.angular-console", - "ms-playwright.playwright", "ms-azuretools.vscode-docker", - "vitest.explorer", - "Codeium.codeium" + "Codeium.codeium", + "Vercel.turbo-vsc" ] } } diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index dadd80bb..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["nrwl.angular-console", "esbenp.prettier-vscode", "ms-playwright.playwright"] -} diff --git a/.vscode/settings.json b/.vscode/settings.json index a7efa1f4..6f9da09f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - "prettier.enable": false, "editor.formatOnSave": false, "editor.codeActionsOnSave": { diff --git a/apps/website/app/components/MenuProfile.vue b/apps/website/app/components/MenuProfile.vue index 548c6611..faa7c511 100644 --- a/apps/website/app/components/MenuProfile.vue +++ b/apps/website/app/components/MenuProfile.vue @@ -5,8 +5,7 @@ class="profile-avatar" @click="handleMenuClick" > -
{{ user }}!
- + Войти @@ -14,10 +13,11 @@ @@ -43,7 +43,6 @@ function handleMenuClick() { } .profile-avatar { - padding: 0.2em; width: 58px; height: 58px; background-color: var(--bronze-4); diff --git a/apps/website/package.json b/apps/website/package.json index 57c7ce9e..1fc24b03 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -26,7 +26,6 @@ "@twurple/eventsub-ws": "^7.1.0", "@twurple/pubsub": "^7.1.0", "howler": "^2.2.4", - "jsonwebtoken": "^9.0.2", "pixi.js": "~8.2.6", "zod": "^3.23.8" }, @@ -36,7 +35,6 @@ "@nuxt/devtools": "^1.3.9", "@nuxt/kit": "^3.10.0", "@types/howler": "^2.2.11", - "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.14.8", "@vueuse/core": "^11.0.1", "@vueuse/nuxt": "^11.0.1", diff --git a/apps/website/public/icons/twitch/112.png b/apps/website/public/icons/twitch/112.png new file mode 100644 index 00000000..bb5e2a75 Binary files /dev/null and b/apps/website/public/icons/twitch/112.png differ diff --git a/apps/website/public/icons/twitch/28.png b/apps/website/public/icons/twitch/28.png new file mode 100644 index 00000000..297f5946 Binary files /dev/null and b/apps/website/public/icons/twitch/28.png differ diff --git a/apps/website/public/icons/twitch/56.png b/apps/website/public/icons/twitch/56.png new file mode 100644 index 00000000..114d61bd Binary files /dev/null and b/apps/website/public/icons/twitch/56.png differ diff --git a/apps/website/server/api/auth/me.get.ts b/apps/website/server/api/auth/me.get.ts index b5fa3c43..bc0f996e 100644 --- a/apps/website/server/api/auth/me.get.ts +++ b/apps/website/server/api/auth/me.get.ts @@ -1,28 +1,12 @@ -import jwt from 'jsonwebtoken' -import type { WebsiteProfile } from '@chat-game/types' +export default defineEventHandler(async (event) => { + const session = await getUserSession(event) -export default defineEventHandler((event) => { - const { public: publicEnv, jwtSecretKey } = useRuntimeConfig() - - const token = getCookie(event, publicEnv.cookieKey) - if (!token) { + if (!session?.user) { throw createError({ statusCode: 401, statusMessage: 'Unauthorized', }) } - try { - const { profile } = jwt.verify(token, jwtSecretKey) as { profile: WebsiteProfile } - return profile - } catch (error) { - if (error instanceof jwt.TokenExpiredError) { - deleteCookie(event, publicEnv.cookieKey, { path: '/' }) - } - } - - throw createError({ - statusCode: 401, - statusMessage: 'Unauthorized', - }) + return session.user }) diff --git a/apps/website/server/api/auth/sign-out.delete.ts b/apps/website/server/api/auth/sign-out.delete.ts deleted file mode 100644 index 558128d0..00000000 --- a/apps/website/server/api/auth/sign-out.delete.ts +++ /dev/null @@ -1,15 +0,0 @@ -export default defineEventHandler((event) => { - const { public: publicEnv } = useRuntimeConfig() - - const token = getCookie(event, publicEnv.cookieKey) - if (!token) { - throw createError({ - statusCode: 401, - statusMessage: 'Unauthorized', - }) - } - - deleteCookie(event, publicEnv.cookieKey, { path: '/' }) - - return { ok: true } -}) diff --git a/apps/website/server/api/auth/twitch.get.ts b/apps/website/server/api/auth/twitch.get.ts index 53fca24c..29bbca00 100644 --- a/apps/website/server/api/auth/twitch.get.ts +++ b/apps/website/server/api/auth/twitch.get.ts @@ -1,19 +1,42 @@ const logger = useLogger('twitch-auth') +interface TwitchUser { + id: string + login: string + display_name: string + type: string + broadcaster_type: 'affiliate' | 'partner' + description: string + profile_image_url: string + offline_image_url: string + view_count: number + email: string + created_at: Date +} + export default oauthTwitchEventHandler({ config: { emailRequired: true, }, - async onSuccess(event, { user }) { - logger.log(JSON.stringify(user)) + async onSuccess(event, result: { user: TwitchUser }) { + logger.success(JSON.stringify(result.user)) + + const repository = new DBRepository() + + const profileInDB = await repository.findOrCreateProfile({ + userId: result.user.id, + userName: result.user.login, + }) await setUserSession(event, { user: { - id: user.id, - twitchId: user.id, - userName: user.userName, + id: profileInDB.id, + twitchId: profileInDB.twitchId, + userName: profileInDB.userName, + imageUrl: result.user.profile_image_url, }, }) + return sendRedirect(event, '/') }, // Optional, will return a json error and 401 status code by default diff --git a/apps/website/server/api/auth/twitch3.get.ts b/apps/website/server/api/auth/twitch3.get.ts deleted file mode 100644 index 7e4b15b4..00000000 --- a/apps/website/server/api/auth/twitch3.get.ts +++ /dev/null @@ -1,71 +0,0 @@ -import jwt from 'jsonwebtoken' -import type { TwitchAccessTokenResponse, WebsiteProfile } from '@chat-game/types' -import { getTokenInfo } from '@twurple/auth' - -export default defineEventHandler(async (event) => { - const { public: publicEnv, jwtSecretKey } = useRuntimeConfig() - const query = getQuery(event) - - if (!query.code) { - throw createError({ - statusCode: 400, - statusMessage: 'Missing code', - }) - } - - const code = query.code.toString() - const twitchResponse = await obtainTwitchAccessToken(code) - if (!twitchResponse?.access_token) { - throw createError({ - statusCode: 401, - statusMessage: 'Unauthorized', - }) - } - - const tokenInfo = await getTokenInfo(twitchResponse.access_token) - if (!tokenInfo.userId || !tokenInfo.userName) { - throw createError({ - statusCode: 400, - statusMessage: 'Wrong userId or userName', - }) - } - - const repository = new DBRepository() - - const profileInDB = await repository.findOrCreateProfile({ - userId: tokenInfo.userId, - userName: tokenInfo.userName, - }) - - const profile: WebsiteProfile = { - id: profileInDB.id, - twitchId: tokenInfo.userId, - userName: tokenInfo.userName, - } - - const token = jwt.sign({ profile }, jwtSecretKey, { expiresIn: '7d' }) - - setCookie(event, publicEnv.cookieKey, token, { - path: '/', - httpOnly: true, - }) - - return sendRedirect(event, '/') -}) - -async function obtainTwitchAccessToken(code: string) { - const { public: publicEnv, oauthTwitchClientId, oauthTwitchClientSecret } = useRuntimeConfig() - - try { - const response = await fetch( - `https://id.twitch.tv/oauth2/token?client_id=${oauthTwitchClientId}&client_secret=${oauthTwitchClientSecret}&code=${code}&grant_type=authorization_code&redirect_uri=${publicEnv.signInRedirectUrl}`, - { - method: 'POST', - }, - ) - return (await response.json()) as TwitchAccessTokenResponse - } catch (err) { - console.error('obtainTwitchAccessToken', err) - return null - } -} diff --git a/apps/website/server/middleware/auth.ts b/apps/website/server/middleware/auth.ts index a598dba9..2c0e3882 100644 --- a/apps/website/server/middleware/auth.ts +++ b/apps/website/server/middleware/auth.ts @@ -10,6 +10,10 @@ export default defineEventHandler((event) => { return } + if (event.path === '/api/_auth/session') { + return + } + if (!token || token !== `Bearer ${websiteBearer}`) { return createError({ statusCode: 403, diff --git a/apps/website/types/auth.d.ts b/apps/website/types/auth.d.ts index 79adb971..4a7a27fb 100644 --- a/apps/website/types/auth.d.ts +++ b/apps/website/types/auth.d.ts @@ -1,8 +1,9 @@ declare module '#auth-utils' { interface User { id: string - twitchId: number + twitchId: string userName: string + imageUrl: string } interface UserSession { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29da6a41..d2c298fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,9 +65,6 @@ importers: howler: specifier: ^2.2.4 version: 2.2.4 - jsonwebtoken: - specifier: ^9.0.2 - version: 9.0.2 pixi.js: specifier: ~8.2.6 version: 8.2.6 @@ -90,9 +87,6 @@ importers: '@types/howler': specifier: ^2.2.11 version: 2.2.11 - '@types/jsonwebtoken': - specifier: ^9.0.6 - version: 9.0.6 '@types/node': specifier: ^20.14.8 version: 20.16.1 @@ -1437,9 +1431,6 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/jsonwebtoken@9.0.6': - resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} - '@types/mdast@3.0.15': resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} @@ -1916,9 +1907,6 @@ 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==} @@ -2439,9 +2427,6 @@ 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==} @@ -3311,16 +3296,6 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} - 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==} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3396,27 +3371,12 @@ 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.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -3429,9 +3389,6 @@ packages: lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - lodash.once@4.1.1: - resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -6630,10 +6587,6 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/jsonwebtoken@9.0.6': - dependencies: - '@types/node': 20.16.1 - '@types/mdast@3.0.15': dependencies: '@types/unist': 2.0.11 @@ -7271,8 +7224,6 @@ snapshots: buffer-crc32@1.0.0: {} - buffer-equal-constant-time@1.0.1: {} - buffer-from@1.1.2: {} buffer@6.0.3: @@ -7782,10 +7733,6 @@ 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.13: {} @@ -8818,30 +8765,6 @@ snapshots: jsonparse@1.3.1: {} - 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 - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -8955,20 +8878,10 @@ 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.kebabcase@4.1.1: {} lodash.memoize@4.1.2: {} @@ -8977,8 +8890,6 @@ snapshots: lodash.mergewith@4.6.2: {} - lodash.once@4.1.1: {} - lodash.snakecase@4.1.1: {} lodash.startcase@4.4.0: {}