diff options
| author | Pinapelz <yukais@pinapelz.com> | 2026-06-03 01:38:53 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2026-06-03 01:38:53 -0700 |
| commit | 4a2f82f06490b7fb277dc6c7558d10c34503a495 (patch) | |
| tree | a206b7dd9777371d07ba1599cf3eff7f52df4d97 | |
| parent | 0cc204ae751a69f20925d2791c930bbf45e4eeed (diff) | |
add basic daily answer encryption
| -rw-r--r-- | .github/actions/build/action.yml | 18 | ||||
| -rw-r--r-- | .github/workflows/build_website.yml | 13 | ||||
| -rw-r--r-- | .github/workflows/deploy_website.yml | 40 | ||||
| -rw-r--r-- | .husky/commit-msg | 4 | ||||
| -rw-r--r-- | server/index.ts | 51 | ||||
| -rw-r--r-- | src/app.tsx | 15 | ||||
| -rw-r--r-- | src/components/Footer/index.tsx | 9 | ||||
| -rw-r--r-- | src/constants/index.ts | 2 | ||||
| -rw-r--r-- | src/data/songs.ts | 4 | ||||
| -rw-r--r-- | src/helpers/fetchSolution.ts | 52 | ||||
| -rw-r--r-- | src/helpers/fetchSongs.ts | 51 |
11 files changed, 172 insertions, 87 deletions
diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml deleted file mode 100644 index e4e161f..0000000 --- a/.github/actions/build/action.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Build Website -runs: - using: "composite" - steps: - - name: Enable Corepack - shell: bash - run: corepack enable - - name: Set Node.js - uses: actions/setup-node@v4 - with: - node-version: 20.x - cache: 'yarn' - - name: Install dependencies - shell: bash - run: yarn install --immutable - - name: Run build - shell: bash - run: yarn build diff --git a/.github/workflows/build_website.yml b/.github/workflows/build_website.yml deleted file mode 100644 index 56a2200..0000000 --- a/.github/workflows/build_website.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Build Website - -on: - pull_request: - branches: [ "master" ] - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/build diff --git a/.github/workflows/deploy_website.yml b/.github/workflows/deploy_website.yml deleted file mode 100644 index 7c0557e..0000000 --- a/.github/workflows/deploy_website.yml +++ /dev/null @@ -1,40 +0,0 @@ -# Simple workflow for deploying static content to GitHub Pages -name: Deploy Website - -on: - # Runs on pushes targeting the default branch - push: - branches: ["master"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Single deploy job since we're just deploying - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/build - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: './build' - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.husky/commit-msg b/.husky/commit-msg deleted file mode 100644 index 617efbd..0000000 --- a/.husky/commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx --no-install commitlint --edit diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..bf4cafd --- /dev/null +++ b/server/index.ts @@ -0,0 +1,51 @@ +import express from 'express'; +import path from 'path'; +import crypto from 'crypto'; +import { songs } from '../src/data/songs'; +import { startDate } from '../src/constants/startDate'; +import cors from 'cors'; + + +const app = express(); +app.use(cors()); +app.use(express.json()); + +const PORT = process.env.PORT || 3001; +const SALT = process.env.HEARDLE_SALT ?? 'changeme'; + +function getDailyKey(): Buffer { + const date = new Date().toISOString().split('T')[0]; + return crypto.pbkdf2Sync(date, SALT, 100_000, 32, 'sha256'); +} + +app.get('/today', (_req, res) => { + const msInDay = 86_400_000; + const index = Math.floor((Date.now() - startDate.getTime()) / msInDay); + const song = songs[index % songs.length]; + + const key = getDailyKey(); + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); + const encrypted = Buffer.concat([ + cipher.update(JSON.stringify(song), 'utf8'), + cipher.final(), + ]); + res.json({ + iv: iv.toString('hex'), + tag: cipher.getAuthTag().toString('hex'), + data: encrypted.toString('hex'), + }); +}); + +app.get('/songs', (_req, res) => { + res.json(songs.map(({ artist, name }) => ({ artist, name }))); +}); + +if (process.env.NODE_ENV === 'production') { + app.use(express.static(path.join(__dirname, '../build'))); + app.get('*', (_req, res) => { + res.sendFile(path.join(__dirname, '../build', 'index.html')); + }); +} + +app.listen(PORT, () => console.log(`Server running on :${PORT}`)); diff --git a/src/app.tsx b/src/app.tsx index 7c4c81d..4eeccc8 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -3,8 +3,8 @@ import _ from "lodash"; import { Song } from "./types/song"; import { GuessState, GuessType } from "./types/guess"; +import { getDailySolution } from "./helpers/fetchSolution"; -import { todaysSolution } from "./helpers"; import { Header, InfoPopUp, Game, Footer } from "./components"; @@ -22,6 +22,7 @@ function App() { const [currentTry, setCurrentTry] = React.useState<number>(0); const [selectedSong, setSelectedSong] = React.useState<Song>(); const [didGuess, setDidGuess] = React.useState<boolean>(false); + const [todaysSolution, setTodaysSolution] = React.useState<Song | null>(null); const firstRun = localStorage.getItem("firstRun") === null; @@ -59,6 +60,10 @@ function App() { let statsVersion = JSON.parse(localStorage.getItem("version") || "1"); React.useEffect(() => { + getDailySolution().then(solution => setTodaysSolution(solution)); + }, []); + + React.useEffect(() => { if (Array.isArray(stats)) { const visitedToday = _.isEqual( todaysSolution, @@ -78,8 +83,6 @@ function App() { setDidGuess(didGuess); } } else { - // initialize stats - // useEffect below does rest stats = []; stats.push({ solution: todaysSolution, @@ -168,7 +171,7 @@ function App() { let state = GuessState.Incorrect; if (selectedSong === todaysSolution) { state = GuessState.Correct; - } else if (selectedSong?.artist === todaysSolution.artist) { + } else if (selectedSong?.artist === todaysSolution?.artist) { state = GuessState.PartiallyCorrect } @@ -195,6 +198,10 @@ function App() { } }, [guesses, selectedSong]); + if (todaysSolution === null) { + return null; + } + return ( <main> <Header openInfoPopUp={openInfoPopUp} /> diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx index e2747c3..3ba4c7c 100644 --- a/src/components/Footer/index.tsx +++ b/src/components/Footer/index.tsx @@ -20,13 +20,8 @@ export function Footer() { return ( <footer> <Styled.Text> - Made with <IoHeart /> by{" "} - <Styled.Link href="https://epicwolverine.com"> - EpicWolverine - </Styled.Link> - {" "}based on the work of{" "} - <Styled.Link href="https://twitter.com/synowski_maciej"> - Maciej Synowski + <Styled.Link href=""> + Source Code </Styled.Link> </Styled.Text> {showDebugButton && diff --git a/src/constants/index.ts b/src/constants/index.ts index bf50a38..9d6bd5f 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,5 +1,5 @@ export { appName } from "./appName"; export { playTimes } from "./playTimes"; -export { songs } from "./songs"; +export { songs } from "../data/songs"; export { startDate } from "./startDate"; export { theme } from "./theme"; diff --git a/src/data/songs.ts b/src/data/songs.ts new file mode 100644 index 0000000..7b760b2 --- /dev/null +++ b/src/data/songs.ts @@ -0,0 +1,4 @@ +export const songs = [ + { artist: "KiiiKiii", name: "Strawberry Cheesegame", youtubeId: "CQSY6FeT68I" }, + { artist: "ifeye", name: "r u ok?", youtubeId: "tEF6aKhfPaQ" }, +]; diff --git a/src/helpers/fetchSolution.ts b/src/helpers/fetchSolution.ts new file mode 100644 index 0000000..b43722a --- /dev/null +++ b/src/helpers/fetchSolution.ts @@ -0,0 +1,52 @@ +import { Song } from "../types/song"; + +const SALT = process.env.HEARDLE_SALT ?? 'changeme'; +const API_URL = process.env.HEARDLE_API_URL ?? 'http://localhost:3001'; + +function hexToBytes(hex: string): Uint8Array { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16); + } + return bytes; +} + + +async function getDailyKey(): Promise<CryptoKey> { + const enc = new TextEncoder(); + const date = new Date().toISOString().split('T')[0]; + const keyMaterial = await crypto.subtle.importKey( + 'raw', + enc.encode(date), + 'PBKDF2', + false, + ['deriveKey'], + ); + return crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: enc.encode(SALT), iterations: 100_000, hash: 'SHA-256' }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['decrypt'], + ); +} + +export async function getDailySolution(): Promise<Song> { + const solutionData = await fetch(`${API_URL}/today`); + if (!solutionData.ok) { + throw new Error(`Failed to fetch solution: ${solutionData.statusText}`); + } + const { iv, tag, data } = await solutionData.json(); + const key = await getDailyKey(); + const ciphertext = hexToBytes(data); + const authTag = hexToBytes(tag); + const combined = new Uint8Array(ciphertext.length + authTag.length); + combined.set(ciphertext); + combined.set(authTag, ciphertext.length); + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: hexToBytes(iv) }, + key, + combined, + ); + return JSON.parse(new TextDecoder().decode(decrypted)) as Song; +} diff --git a/src/helpers/fetchSongs.ts b/src/helpers/fetchSongs.ts new file mode 100644 index 0000000..5ae0d83 --- /dev/null +++ b/src/helpers/fetchSongs.ts @@ -0,0 +1,51 @@ +import { Song } from "../types/song"; +let cachedSongs: Song[] | null = null; +function fuzzyMatch(input: string): string { + return input.toLowerCase().replace(/[^0-9a-z ]/gi, ''); +} + + +export async function fetchSongs(useCache=true): Promise<Song[]> { + if (useCache && cachedSongs) { + return cachedSongs; + } + + try { + const response = await fetch('/songs'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const songsData: Song[] = await response.json(); + cachedSongs = songsData; + return songsData; + } catch (error) { + console.error("Failed to fetch songs:", error); + throw error; + } +} + +export async function searchSongs(searchTerm: string): Promise<Song[]> { + const songsToSearch = await fetchSongs(); + + const processedSearchTerm = fuzzyMatch(searchTerm); + + const matchingSongs = songsToSearch + .filter((song: Song) => { + const songName = fuzzyMatch(song.name); + const songArtist = fuzzyMatch(song.artist); + + if (songArtist.includes(processedSearchTerm) || songName.includes(processedSearchTerm)) { + return true; + } + return false; + }) + .sort((a, b) => + a.artist.toLowerCase().localeCompare(b.artist.toLocaleLowerCase()) + || a.name.toLowerCase().localeCompare(b.name.toLocaleLowerCase()) + ); + + return matchingSongs; +} +export function getCachedSongs(): Song[] | null { + return cachedSongs; +} |
