Add in Discord OAuth and show player cards to authenticated users

This commit is contained in:
Peter 2023-08-23 19:54:34 -04:00
parent 4fb2d7b4c8
commit cddc982e8b
6 changed files with 138 additions and 20 deletions

View File

@ -1,6 +1,6 @@
<template> <template>
<nav class="navbar navbar-expand-sm" style="margin-bottom: 1rem" id="navbar"> <nav class="navbar navbar-expand-sm" style="margin-bottom: 1rem" id="navbar">
<RouterLink class="navbar-brand nav-link" to="/">SBa Season {{ seasonNumber }}</RouterLink> <RouterLink class="navbar-brand nav-link" to="/">SBa Season {{ seasonNumber() }}</RouterLink>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#top-navbar-collapse" <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#top-navbar-collapse"
aria-controls="top-navbar-collapse" aria-expanded="false" aria-label="Toggle navigation"> aria-controls="top-navbar-collapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
@ -13,7 +13,7 @@
</li> </li>
<li class="nav-item"> <li class="nav-item">
<RouterLink class="nav-link" <RouterLink class="nav-link"
:to="{ name: 'team', params: { seasonNumber: seasonNumber, teamAbbreviation: 'FA' } }">Free Agents :to="{ name: 'team', params: { seasonNumber: seasonNumber(), teamAbbreviation: 'FA' } }">Free Agents
</RouterLink> </RouterLink>
</li> </li>
<li class="nav-item"> <li class="nav-item">
@ -23,11 +23,16 @@
<a class="nav-link" target="_blank" href="https://sbanews.manticorum.com/">News</a> <a class="nav-link" target="_blank" href="https://sbanews.manticorum.com/">News</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a id="login" class="nav-link">Login with Discord</a> <button v-if="!isAuthenticated" id="login" class="nav-link" @click="authenticate">Login with Discord</button>
</li> </li>
<li class="nav-item"> <li v-if="false" class="nav-item">
<!-- TODO RouterLink to team page with team icon --> <!-- make the above true if you want to clear cookies for testing -->
<a id="team-login-link" href="/teams?abbrev="></a> <button class="nav-link" @click="clearCookie">Clear Cookie</button>
</li>
<li v-if="isAuthenticated && userTeam" class="nav-item">
<RouterLink :to="{ name: 'team', params: { seasonNumber: seasonNumber(), teamAbbreviation: userTeam.abbrev } }">
<img id="thumbnail" style="max-height: 35px; float:right; vertical-align:middle" :src=userTeam?.thumbnail>
</RouterLink>
</li> </li>
</ul> </ul>
@ -68,17 +73,23 @@ import { RouterLink } from 'vue-router'
import type { MenuOption } from 'naive-ui' import type { MenuOption } from 'naive-ui'
import { fetchPlayers, type Player } from '@/services/playersService' import { fetchPlayers, type Player } from '@/services/playersService'
import { CURRENT_SEASON } from '@/services/utilities' import { CURRENT_SEASON } from '@/services/utilities'
import type { Team } from '@/services/apiResponseTypes'
import { fetchActiveTeamByOwnerId } from '@/services/teamsService'
import { authenticate, clearCookie, completeAuthentication, getOwnerId, isDiscordAuthenticated, parseCookie } from '@/services/authenticationService'
export default { export default {
name: 'NavBar', name: 'NavBar',
data() { data() {
return { return {
userTeam: undefined as Team | undefined,
players: [] as Player[], players: [] as Player[],
searchPlayerName: undefined searchPlayerName: undefined
} }
}, },
created() { created() {
this.populatePlayerNames() this.populatePlayerNames()
this.completeAuthentication()
this.fetchUserTeam()
}, },
computed: { computed: {
menuOptions(): MenuOption[] { menuOptions(): MenuOption[] {
@ -267,23 +278,43 @@ export default {
{ label: 'News', key: 'News' } { label: 'News', key: 'News' }
] ]
}, },
seasonNumber(): number {
// TODO pull this from DB?
return CURRENT_SEASON
},
sortedPlayerNames(): string[] { sortedPlayerNames(): string[] {
return this.players.sort((p1, p2) => p2.wara - p1.wara).map(p => p.name) return this.players.sort((p1, p2) => p2.wara - p1.wara).map(p => p.name)
},
isAuthenticated(): boolean {
return isDiscordAuthenticated()
},
ownerId(): string | undefined {
return getOwnerId()
} }
}, },
methods: { methods: {
async getPlayers(): Promise<Player[]> { async getPlayers(): Promise<Player[]> {
return await fetchPlayers(this.seasonNumber) return await fetchPlayers(this.seasonNumber())
}, },
async populatePlayerNames(): Promise<void> { async populatePlayerNames(): Promise<void> {
this.players = await this.getPlayers() this.players = await this.getPlayers()
}, },
searchPlayers(): void { searchPlayers(): void {
this.$router.push({ path: `/players/${this.seasonNumber}/${this.searchPlayerName}` }) this.$router.push({ path: `/players/${this.seasonNumber()}/${this.searchPlayerName}` })
},
authenticate(): void {
authenticate()
},
async completeAuthentication(): Promise<void> {
if (this.ownerId) return
completeAuthentication()
},
async fetchUserTeam(): Promise<void> {
if (!this.ownerId) return
this.userTeam = await fetchActiveTeamByOwnerId(this.ownerId)
},
seasonNumber(): number {
return CURRENT_SEASON
},
clearCookie(): void {
clearCookie()
} }
} }
} }

View File

@ -0,0 +1,67 @@
import { SITE_URL } from './utilities'
export function authenticate(): void {
// MAJOR DOMO - 712002920950005870
const clientID = '712002920950005870'
// TODO: dynamically redirect to current view by getting current query params and adding them back on after?
const redirectURI = encodeURIComponent(SITE_URL)
const scope = 'identify' // Adjust the scope as needed
window.location.href = `https://discord.com/oauth2/authorize?client_id=${clientID}&redirect_uri=${redirectURI}&response_type=token&scope=${scope}`
}
export function isDiscordAuthenticated(): boolean {
return !!getOwnerId()
}
export function getOwnerId(): string | undefined {
return parseCookie(document.cookie)?.discord
}
export async function completeAuthentication(): Promise<void> {
if (!window.location.hash) {
console.warn('No token hash found in return URL')
return
}
// Extract the access token from the URL params
const urlParams = new URLSearchParams(window.location.hash.slice(1))
const accessToken = urlParams.get('access_token')
const tokenType = urlParams.get('token_type') //should be 'Bearer'
const userResponse = await fetch('https://discord.com/api/users/@me', {
headers: {
authorization: `${tokenType} ${accessToken}`
}
})
if (!userResponse.ok) {
console.warn('userResponse was not OK', userResponse)
return
}
// right now we only care about the id property but we could make a full discord account object if desired
const user: { id: string } = await userResponse.json()
if (!user?.id) {
console.warn('No user or id found while authenticating')
return
}
window.location.href = SITE_URL
saveOwnerIdCookie(user.id)
}
// Based on https://www.30secondsofcode.org/js/s/parse-cookie/
export function parseCookie(cookie: string | undefined): { [key: string]: string } {
if (!cookie) return {}
return cookie.split(';')
.map(v => v.split('='))
.reduce((acc: { [key: string]: string }, v) => {
acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim())
return acc
}, {})
}
export function saveOwnerIdCookie(ownerId: string): void {
document.cookie = `discord=${ownerId}; expires=Fri, 1 Jan 2100 12:00:00 UTC; path=/`
}
export function clearCookie(): void {
document.cookie = "discord=;-1;path=/"
}

View File

@ -1,5 +1,5 @@
import type { Team } from './apiResponseTypes' import type { Team } from './apiResponseTypes'
import { SITE_URL } from './utilities' import { CURRENT_SEASON, SITE_URL } from './utilities'
export async function fetchTeam(seasonNumber: number, teamAbbreviation: string): Promise<Team> { export async function fetchTeam(seasonNumber: number, teamAbbreviation: string): Promise<Team> {
const response = await fetch(`${SITE_URL}/api/v3/teams?season=${seasonNumber}&team_abbrev=${teamAbbreviation}`) const response = await fetch(`${SITE_URL}/api/v3/teams?season=${seasonNumber}&team_abbrev=${teamAbbreviation}`)
@ -15,3 +15,19 @@ export async function fetchTeam(seasonNumber: number, teamAbbreviation: string):
return teamResponse.teams[0] return teamResponse.teams[0]
} }
export async function fetchActiveTeamByOwnerId(ownerId: string): Promise<Team | undefined> {
const response = await fetch(`${SITE_URL}/api/v3/teams?season=${CURRENT_SEASON}&active_only=True&owner_id=${ownerId}`)
const teamResponse: {
count: number
teams: Team[]
} = await response.json()
if (teamResponse.count === 0) {
console.warn('teamServices.fetchActiveTeamByOwnerId - Received 0 active teams for owner id, are they active?')
return undefined
}
return teamResponse.teams[0]
}

View File

@ -59,6 +59,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { isDiscordAuthenticated } from '@/services/authenticationService'
import { fetchLast2DecisionsByPlayerId, type Decision } from '@/services/decisionsService' import { fetchLast2DecisionsByPlayerId, type Decision } from '@/services/decisionsService'
import { type Player, fetchPlayerByName } from '@/services/playersService' import { type Player, fetchPlayerByName } from '@/services/playersService'
@ -75,9 +76,8 @@ export default {
playerName: { type: String, required: true } playerName: { type: String, required: true }
}, },
computed: { computed: {
isAuthorized(): boolean { isAuthenticated(): boolean {
//TODO check discord oauth/cookie/token return isDiscordAuthenticated()
return false
}, },
teamAbbreviation(): string | undefined { teamAbbreviation(): string | undefined {
return this.player?.team?.abbrev return this.player?.team?.abbrev
@ -94,11 +94,11 @@ export default {
return this.player?.vanity_card ?? this.teamThumbnail return this.player?.vanity_card ?? this.teamThumbnail
}, },
playerCardImage1Url(): string | undefined { playerCardImage1Url(): string | undefined {
if (!this.isAuthorized) return undefined if (!this.isAuthenticated) return undefined
return this.player?.image return this.player?.image
}, },
playerCardImage2Url(): string | undefined { playerCardImage2Url(): string | undefined {
if (!this.isAuthorized) return undefined if (!this.isAuthenticated) return undefined
return this.player?.image2 return this.player?.image2
}, },
injuryReturnDate(): string | undefined { injuryReturnDate(): string | undefined {

View File

@ -2,7 +2,7 @@
<div class="team-view centerDiv"> <div class="team-view centerDiv">
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
<h1 id="team-record">{{ teamName }}</h1> <h1 id="team-record">{{ teamName }} {{ teamRecord }}</h1>
<h2 id="standings"></h2> <h2 id="standings"></h2>
<h2 id="streak">Last 8: {{ lastEight }} / Streak: {{ streak }}</h2> <h2 id="streak">Last 8: {{ lastEight }} / Streak: {{ streak }}</h2>
</div> </div>
@ -199,6 +199,10 @@ export default {
teamName(): string | undefined { teamName(): string | undefined {
return this.team?.lname return this.team?.lname
}, },
teamRecord(): string | undefined {
if (!this.teamStanding) return undefined
return `(${this.teamStanding?.wins}-${this.teamStanding.losses})`
},
minorsTeamName(): string | undefined { minorsTeamName(): string | undefined {
return this.teamMinors?.sname return this.teamMinors?.sname
}, },