Skip to content

Commit

Permalink
Twitch authentication
Browse files Browse the repository at this point in the history
Fixes #92
  • Loading branch information
Perdolique committed Sep 3, 2024
1 parent 3fd21d8 commit 7e6646f
Show file tree
Hide file tree
Showing 32 changed files with 1,197 additions and 147 deletions.
22 changes: 22 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"recommendations": [
"aaron-bond.better-comments",
"antfu.iconify",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"github.copilot",
"github.copilot-chat",
"github.vscode-github-actions",
"ms-azuretools.vscode-docker",
"mtxr.sqltools",
"mtxr.sqltools-driver-pg",
"pflannery.vscode-versionlens",
"ritwickdey.liveserver",
"tamasfe.even-better-toml",
"visualstudioexptteam.intellicode-api-usage-examples",
"visualstudioexptteam.vscodeintellicode",
"vue.volar",
"vunguyentuan.vscode-css-variables",
"yoavbls.pretty-ts-errors"
]
}
2 changes: 1 addition & 1 deletion app/composables/use-equipment-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { EquipmentItem } from '~/components/equipment/EquipmentTable.vue';
interface EquipmentData {
readonly id: string;
readonly name: string | null;
readonly weight: number | null;
readonly weight: number;
readonly createdAt: string;
}

Expand Down
6 changes: 5 additions & 1 deletion app/middleware/0.user.global.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export default defineNuxtRouteMiddleware(async () => {
export default defineNuxtRouteMiddleware(async (to) => {
if (shouldSkipAuth(to)) {
return
}

if (import.meta.server) {
const { getUser, user } = useUserStore()

Expand Down
4 changes: 4 additions & 0 deletions app/middleware/1.auth.global.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { startPagePath } from '~~/constants';

export default defineNuxtRouteMiddleware(async (to) => {
if (shouldSkipAuth(to)) {
return
}

const { isAuthenticated } = useUserStore()

if (isAuthenticated.value && to.path === '/login') {
Expand Down
1 change: 1 addition & 0 deletions app/models/oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type OAuthProvider = 'twitch'
25 changes: 25 additions & 0 deletions app/models/twitch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export interface TwitchUser {
readonly id: string;
readonly login: string;
readonly display_name: string;
readonly type: '' | 'staff' | 'admin' | 'global_mod';
readonly broadcaster_type: '' | 'affiliate' | 'partner';
readonly description: string;
readonly profile_image_url: string;
readonly offline_image_url: string;
readonly created_at: string;
// "user:read:email" scope required
readonly email?: string;
}

export interface TwitchOAuthTokenResponse {
readonly access_token: string;
readonly expires_in: number;
readonly refresh_token: string;
readonly token_type: string;
readonly scope?: string[];
}

export interface TwitchUsersResponse {
readonly data: TwitchUser[];
}
109 changes: 109 additions & 0 deletions app/pages/auth/twitch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<template>
<div :class="$style.component">
<div :class="$style.content">
<div :class="$style.progress">
<div
v-if="isFailed"
:class="$style.icon"
>
💀
</div>

<CircleSpinner
v-else
/>
</div>

<div v-if="isFailed">
Failed to connect <strong>Twitch</strong>
</div>

<div v-else>
Connecting <strong>Twitch</strong>...
</div>

<PerdLink
v-if="isFailed"
to="/"
>
Return to Home
</PerdLink>
</div>
</div>
</template>

<script lang="ts" setup>
import { startPagePath } from '~~/constants'
import CircleSpinner from '~/components/CircleSpinner.vue'
import PerdLink from '~/components/PerdLink.vue';
definePageMeta({
layout: false,
skipAuth: true
})
const isFailed = useState(() => false)
const route = useRoute()
const { user } = useUserStore()
async function handleConnect() {
try {
const result = await $fetch('/api/oauth/twitch', {
method: 'POST',
body: JSON.stringify({
code: route.query.code
})
})
if (result === undefined) {
throw new Error('Failed to connect Twitch')
}
user.value.userId = result.userId
user.value.isAdmin = result.isAdmin
user.value.hasData = true
await navigateTo(startPagePath, {
replace: true
})
} catch (error) {
isFailed.value = true
}
}
onBeforeMount(() => {
handleConnect()
})
</script>

<style module>
.component {
width: 100vw;
height: 100vh;
display: flex;
overflow: hidden;
padding: var(--spacing-24);
}
.content {
display: grid;
row-gap: var(--spacing-16);
margin: auto;
justify-items: center;
text-align: center;
text-wrap: balance;
}
.progress {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
}
.icon {
font-size: 32px;
}
</style>
37 changes: 13 additions & 24 deletions app/pages/login.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
<template>
<div :class="$style.component">
<PerdButton @click="handleSignInClick">
Sign in anonymously
<PerdButton @click="signUp">
Sign up anonymously
</PerdButton>

<PerdButton @click="handleSignInAdminClick">
Sign in as admin
</PerdButton>
<PerdLink
to="/api/oauth/twitch"
external
>
Sign in with Twitch
</PerdLink>
</div>
</template>

<script lang="ts" setup>
import PerdButton from '~/components/PerdButton.vue';
import PerdLink from '~/components/PerdLink.vue';
definePageMeta({
layout: 'page'
Expand All @@ -20,23 +24,15 @@
const { user } = useUserStore()
const route = useRoute()
async function signIn(isAdmin: boolean) {
async function signUp() {
const response = await $fetch('/api/auth/create-session', {
method: 'POST',
body: {
isAdmin
}
method: 'POST'
})
if (typeof response.userId === 'string') {
user.value.userId = response.userId
}
if (response.isAdmin === true) {
user.value.isAdmin = true
}
const redirectPath =
typeof route.query.redirectTo === 'string'
? route.query.redirectTo
Expand All @@ -46,20 +42,13 @@
replace: true
})
}
async function handleSignInClick() {
await signIn(false)
}
async function handleSignInAdminClick() {
await signIn(true)
}
</script>

<style module>
.component {
display: flex;
display: grid;
justify-content: center;
text-align: center;
gap: var(--spacing-16);
padding-top: var(--spacing-32);
}
Expand Down
25 changes: 0 additions & 25 deletions app/utils/fetch.ts

This file was deleted.

5 changes: 5 additions & 0 deletions app/utils/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { RouteLocationNormalized } from 'vue-router'

export function shouldSkipAuth(to: RouteLocationNormalized) {
return to.meta.skipAuth === true
}
4 changes: 3 additions & 1 deletion constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ export const sessionSecret = 'e2353edd-0a91-4d60-bf3a-715fab5f6c00'
export const startPagePath = '/'

export const publicApiPaths = [
'/auth/create-session'
'/auth/create-session',
'/oauth',
'/oauth/twitch'
]

export const limits = {
Expand Down
35 changes: 34 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,44 @@ services:
POSTGRES_PASSWORD: perd
POSTGRES_USER: perd
POSTGRES_DB: perd
command: postgres -c max_connections=1000
command: -c max_connections=1000
restart: unless-stopped
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: [ 'CMD-SHELL', 'pg_isready -U perd' ]
interval: 10s
timeout: 5s
retries: 5

# https://github.com/neondatabase/neon/issues/4989
# https://github.com/neondatabase/serverless/issues/33#issuecomment-1634853042
# https://github.com/TimoWilhelm/local-neon-http-proxy/tree/main
# https://readme.localtest.me/
neon-proxy:
image: ghcr.io/timowilhelm/local-neon-http-proxy:main
environment:
- PG_CONNECTION_STRING=postgres://perd:perd@postgres:5432/perd
restart: unless-stopped
ports:
- '4444:4444'
depends_on:
postgres:
condition: service_healthy

neon-wsproxy:
image: ghcr.io/neondatabase/wsproxy:latest
environment:
APPEND_PORT: postgres:5432
ALLOW_ADDR_REGEX: .*
LOG_TRAFFIC: true
ports:
- "5433:80"
depends_on:
postgres:
condition: service_healthy

volumes:
postgres_data:
4 changes: 4 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export default defineNuxtConfig({
viewTransition: true
},

typescript: {
strict: true
},

devtools: {
enabled: true
},
Expand Down
Loading

0 comments on commit 7e6646f

Please sign in to comment.