Skip to content

Commit

Permalink
feat: minecraft account linking
Browse files Browse the repository at this point in the history
closes #50
  • Loading branch information
cyyynthia committed Mar 18, 2022
1 parent 650ab02 commit deed1e8
Show file tree
Hide file tree
Showing 17 changed files with 252 additions and 47 deletions.
3 changes: 2 additions & 1 deletion config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"github": [ "client id", "client secret", "token" ],
"twitch": [ "client id", "client secret" ],
"twitter": [ "client id", "client secret" ],
"facebook": [ "client id", "client secret" ]
"facebook": [ "client id", "client secret" ],
"microsoft": [ "client id", "client secret" ]
}
}
2 changes: 2 additions & 0 deletions packages/api/src/oauth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type { FastifyInstance } from 'fastify'
import discordModule from './discord.js'
import facebookModule from './facebook.js'
import githubModule from './github.js'
import minecraftModule from './minecraft.js'
import twitchModule from './twitch.js'
import twitterModule from './twitter.js'

Expand All @@ -40,6 +41,7 @@ export default async function (fastify: FastifyInstance) {
fastify.register(discordModule, { prefix: '/discord' })
fastify.register(facebookModule, { prefix: '/facebook' })
fastify.register(githubModule, { prefix: '/github' })
fastify.register(minecraftModule, { prefix: '/minecraft' })
fastify.register(twitchModule, { prefix: '/twitch' })
fastify.register(twitterModule, { prefix: '/twitter' })
}
124 changes: 124 additions & 0 deletions packages/api/src/oauth/minecraft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2020-2022 Cynthia K. Rey, All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import type { FastifyInstance } from 'fastify'
import type { ExternalAccount } from '@pronoundb/shared'

import fetch from 'node-fetch'
import register from './abstract/oauth2.js'
import config from '../config.js'

const [ clientId, clientSecret ] = config.oauth.microsoft

async function getSelf (token: string): Promise<ExternalAccount | null> {
// Sign into Xbox Live
const xliveReq = await fetch('https://user.auth.xboxlive.com/user/authenticate', {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({
Properties: {
AuthMethod: 'RPS',
SiteName: 'user.auth.xboxlive.com',
RpsTicket: `d=${token}`,
},
RelyingParty: 'http://auth.xboxlive.com',
TokenType: 'JWT',
}),
})

if (!xliveReq.ok) return null
const xlive = await xliveReq.json() as any

// Get a security token
const xstsReq = await fetch('https://xsts.auth.xboxlive.com/xsts/authorize', {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({
Properties: {
SandboxId: 'RETAIL',
UserTokens: [ xlive.Token ],
},
RelyingParty: 'rp://api.minecraftservices.com/',
TokenType: 'JWT',
}),
})

if (!xstsReq.ok) {
console.log('xsts error', xstsReq.status, await xstsReq.text())
return null
}
const xsts = await xstsReq.json() as any

// Sign into Minecraft
const minecraftReq = await fetch('https://api.minecraftservices.com/authentication/login_with_xbox', {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({
identityToken: `XBL3.0 x=${xsts.DisplayClaims.xui[0].uhs};${xsts.Token}`,
ensureLegacyEnabled: true,
}),
})

if (!minecraftReq.ok) {
console.log('mc error', minecraftReq.status, await minecraftReq.text())
return null
}
const minecraftToken = await minecraftReq.json() as any

// User data wooo
const data = await fetch('https://api.minecraftservices.com/minecraft/profile', {
headers: {
authorization: `Bearer ${minecraftToken.access_token}`,
accept: 'application/json',
},
}).then((r) => r.json() as any)

const uuid = `${data.id.slice(0, 8)}-${data.id.slice(8, 12)}-${data.id.slice(12, 16)}-${data.id.slice(16, 20)}-${data.id.slice(20)}`
return { id: uuid, name: data.name, platform: 'minecraft' }
}

export default async function (fastify: FastifyInstance) {
register(fastify, {
clientId: clientId,
clientSecret: clientSecret,
platform: 'twitch',
authorization: 'https://login.live.com/oauth20_authorize.srf',
token: 'https://login.live.com/oauth20_token.srf',
scopes: [ 'XboxLive.signin' ],
getSelf: getSelf,
})
}
9 changes: 9 additions & 0 deletions packages/shared/assets/minecraft.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/shared/src/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import Facebook from 'simple-icons/icons/facebook.svg'
import GitHub from 'simple-icons/icons/github.svg'
import Instagram from 'simple-icons/icons/instagram.svg'
import Mastodon from 'simple-icons/icons/mastodon.svg'
import Minecraft from '../assets/minecraft.svg'
import Osu from 'simple-icons/icons/osu.svg'
import Reddit from 'simple-icons/icons/reddit.svg'
import Twitch from 'simple-icons/icons/twitch.svg'
Expand All @@ -44,6 +45,7 @@ const Icons = {
github: GitHub,
instagram: Instagram,
mastodon: Mastodon,
minecraft: Minecraft,
osu: Osu,
reddit: Reddit,
twitch: Twitch,
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/platforms.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ const Platforms = {
since: '0.0.0',
soon: true,
},
minecraft: {
name: 'Minecraft',
color: '#854F2B',
since: '0.0.0',
},
osu: {
name: 'osu!',
color: '#FF66AA',
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ declare module '@pronoundb/shared' {
since: string
requiresExt?: boolean
soon?: boolean
info?: string
}

export type User = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"associatedApplications": [
{ "applicationId": "7b7f1107-2134-4c62-8d64-51ff36e4bd6f" }
]
}
2 changes: 1 addition & 1 deletion packages/website/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import NotFound from './pages/NotFound'

import { Routes, Errors } from '../constants'

// import logo from '../assets/powercord.png'
// import logo from '../assets/logo.png'

type AppProps = { user?: User, url?: string, error?: string | null }

Expand Down
2 changes: 1 addition & 1 deletion packages/website/src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import { h } from 'preact'

import { Routes } from '../constants'
import useHeart from '../useHeart'
import useHeart from '../hooks/useHeart'

import Paw from '/assets/paw.svg'

Expand Down
73 changes: 33 additions & 40 deletions packages/website/src/components/account/Auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,21 @@
*/

import type { Attributes } from 'preact'
import { h } from 'preact'
import { useRef, useCallback, useState, useEffect, useContext } from 'preact/hooks'
import { h, Fragment } from 'preact'
import { useState, useEffect, useContext } from 'preact/hooks'
import { useMeta, useTitle } from 'hoofd/preact'
import { route } from 'preact-router'
import { Platforms, PlatformIds } from '@pronoundb/shared/platforms.js'
import PlatformIcons from '@pronoundb/shared/icons.js'
import useTooltip from '../../hooks/useTooltip'
import { compareSemver } from '../../util'

import UserContext from '../UserContext'
import AppContext from '../AppContext'
import { Routes, Endpoints } from '../../constants'

import Info from 'feather-icons/dist/icons/info.svg'

type OAuthIntent = 'login' | 'register' | 'link'

type OAuthProps = Attributes & { intent: OAuthIntent }
Expand All @@ -53,8 +56,12 @@ function LinkButton ({ platformId, intent }: { platformId: string, intent: OAuth
const getPdbExtVer = () => import.meta.env.SSR ? void 0 : window.__PRONOUNDB_EXTENSION_VERSION__
const platform = Platforms[platformId]

const divRef = useRef<HTMLDivElement>(null)
const tooltipRef = useRef<HTMLDivElement>()
const extMessage = getPdbExtVer()
? `You need to update the PronounDB extension to link a ${platform.name} account.`
: `You need to install the PronounDB extension to link a ${platform.name} account.`
const [ buttonRef, onMouseEnterButton, onMouseLeaveButton ] = useTooltip(extMessage)
const [ infoRef, onMouseEnterInfo, onMouseLeaveInfo ] = useTooltip(platform.info ?? '')

const [ disabled, setDisabled ] = useState(platform.requiresExt)

function check () {
Expand All @@ -69,46 +76,33 @@ function LinkButton ({ platformId, intent }: { platformId: string, intent: OAuth
}
}, [ platformId ])

const onMouseIn = useCallback(() => {
const { x, y, width } = divRef.current!.getBoundingClientRect()
const tt = document.createElement('div')
tt.className = 'tooltip'
tt.style.left = `${x + (width / 2)}px`
tt.style.top = `${y}px`
tt.style.opacity = '0'
tt.innerText = getPdbExtVer()
? `You need to update the PronounDB extension to link a ${platform.name} account.`
: `You need to install the PronounDB extension to link a ${platform.name} account.`
document.body.appendChild(tt)

setTimeout(() => (tt.style.opacity = '1'), 0)
tooltipRef.current = tt
}, [ disabled ])

const onMouseOut = useCallback(() => {
const tooltip = tooltipRef.current
if (!tooltip) return

tooltip.style.opacity = '0'
setTimeout(() => tooltip.remove(), 150)
}, [ tooltipRef ])

useEffect(() => {
if (!disabled && tooltipRef.current) {
tooltipRef.current.remove()
}
}, [ disabled, tooltipRef.current ])
if (!disabled) onMouseLeaveButton()
}, [ disabled, onMouseLeaveButton ])

const contents = (
<Fragment>
{h(PlatformIcons[platformId], { class: 'w-8 h-8 mr-4 flex-none' })}
<span class='font-semibold flex-1'>Connect with {platform.name}</span>
{platform.info && (
<Info
ref={infoRef}
class='w-4 h-4 ml-4 flex-none'
onMouseEnter={onMouseEnterInfo}
onMouseLeave={onMouseLeaveInfo}
/>
)}
</Fragment>
)
if (disabled) {
return (
<div
ref={divRef}
ref={buttonRef}
class={`platform-box cursor-not-allowed opacity-60 border-platform-${platformId}`}
onMouseEnter={onMouseIn}
onMouseLeave={onMouseOut}
onMouseEnter={onMouseEnterButton}
onMouseLeave={onMouseLeaveButton}
>
{h(PlatformIcons[platformId], { class: 'w-8 h-8 mr-4 flex-none' })}
<span class='font-semibold'>Connect with {platform.name}</span>
{contents}
</div>
)
}
Expand All @@ -120,8 +114,7 @@ function LinkButton ({ platformId, intent }: { platformId: string, intent: OAuth
class={`platform-box border-platform-${platformId}`}
href={Endpoints.OAUTH(platformId, intent)}
>
{h(PlatformIcons[platformId], { class: 'w-8 h-8 mr-4 flex-none' })}
<span class='font-semibold'>Connect with {platform.name}</span>
{contents}
</a>
)
}
Expand All @@ -144,7 +137,7 @@ export default function Auth (props: OAuthProps) {
return (
<main class='container-main'>
<div class='title-context'>Authentication</div>
<h2 class='text-2xl font-bold mb-6'>{IntentTitles[props.intent]}</h2>
<h2 class='text-2xl font-bold mb-4'>{IntentTitles[props.intent]}</h2>
{props.intent === 'login' && <p class='mb-2'>Make sure to select an account you already linked on PronounDB.</p>}
{props.intent === 'register' && <p class='mb-2'>Make sure to give the <a class='link' href={Routes.PRIVACY}>Privacy Policy</a> a look. Registering an account on PronounDB will be seen as an acceptance of it.</p>}

Expand Down
2 changes: 1 addition & 1 deletion packages/website/src/components/extension/Onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { h, Fragment } from 'preact'
import { useContext } from 'preact/hooks'
import { useTitleTemplate } from 'hoofd/preact'

import useHeart from '../../useHeart'
import useHeart from '../../hooks/useHeart'
import UserContext from '../UserContext'
import { Routes } from '../../constants'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import type { Attributes } from 'preact'
import { h, Fragment } from 'preact'

import useHeart from '../../../useHeart'
import useHeart from '../../../hooks/useHeart'
import { Routes } from '../../../constants'

export default function Changelog060 (_: Attributes) {
Expand Down
2 changes: 1 addition & 1 deletion packages/website/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
*/

import { Endpoints as SharedEndpoints, Extensions } from '@pronoundb/shared/constants.js'
import useRandom from './useRandom'
import useRandom from './hooks/useRandom'

export const Routes = {
HOME: '/',
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
*/

import { useContext, useMemo } from 'preact/hooks'
import AppContext from './components/AppContext'
import AppContext from '../components/AppContext'

export default function useRandom (id: string, max: number) {
const ctxId = `random.${id}`
Expand Down
Loading

0 comments on commit deed1e8

Please sign in to comment.