├── .env.example ├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── License.txt ├── README.md ├── app ├── api-strategy.server.ts ├── api.server.ts ├── app-defaults.ts ├── app.ts ├── auth.server.ts ├── components │ ├── app-context.tsx │ ├── applied-sticker-editor.tsx │ ├── apply-item-patch.tsx │ ├── apply-item-sticker.tsx │ ├── background.tsx │ ├── button-with-tooltip.tsx │ ├── cloudflare-analytics-script.tsx │ ├── console.tsx │ ├── context-button.tsx │ ├── context-divider.tsx │ ├── craft-edit.tsx │ ├── craft-new.tsx │ ├── craft-share-user.tsx │ ├── craft-view.tsx │ ├── donate-header-link.tsx │ ├── editor-input.tsx │ ├── editor-item-display.tsx │ ├── editor-label.tsx │ ├── editor-range.tsx │ ├── editor-select.tsx │ ├── editor-step-range-with-input.tsx │ ├── editor-step-range.tsx │ ├── editor-toggle.tsx │ ├── error-boundary.tsx │ ├── fill-spinner.tsx │ ├── footer.tsx │ ├── grid-list.tsx │ ├── header-link.tsx │ ├── header.tsx │ ├── hooks │ │ ├── use-apply-item-patch.ts │ │ ├── use-apply-item-sticker.ts │ │ ├── use-checkbox.ts │ │ ├── use-counter.ts │ │ ├── use-craft-filter-rules.ts │ │ ├── use-craftable-item.ts │ │ ├── use-detect-collision.ts │ │ ├── use-floating-click.ts │ │ ├── use-freeze.ts │ │ ├── use-input.ts │ │ ├── use-inspect-floating.ts │ │ ├── use-inspect-item.ts │ │ ├── use-inventory-filter-state.ts │ │ ├── use-inventory-item-floating.ts │ │ ├── use-inventory-item.ts │ │ ├── use-inventory-state.ts │ │ ├── use-is-desktop.ts │ │ ├── use-is-item-craftable.ts │ │ ├── use-is-on-top.ts │ │ ├── use-item-picker-state.ts │ │ ├── use-key-release.ts │ │ ├── use-key-values.ts │ │ ├── use-listen-app-event.ts │ │ ├── use-lock-scroll.ts │ │ ├── use-name-item.test.ts │ │ ├── use-name-item.ts │ │ ├── use-remove-item-patch.ts │ │ ├── use-rename-item.ts │ │ ├── use-responsive-scale.ts │ │ ├── use-root-layout.ts │ │ ├── use-scrape-item-sticker.ts │ │ ├── use-storage-input.ts │ │ ├── use-storage-state.ts │ │ ├── use-storage-unit.ts │ │ ├── use-swap-items-stattrak.ts │ │ ├── use-sync-state.ts │ │ ├── use-sync.ts │ │ ├── use-timed-state.ts │ │ ├── use-timer.ts │ │ ├── use-translation.ts │ │ ├── use-unlock-case.ts │ │ └── use-watch.ts │ ├── icon-button.tsx │ ├── icon-input.tsx │ ├── icon-select.tsx │ ├── info-icon.tsx │ ├── inspect-item.tsx │ ├── inventory-filter-button.tsx │ ├── inventory-filter.tsx │ ├── inventory-grid-placeholder.tsx │ ├── inventory-item-context-menu.tsx │ ├── inventory-item-tile-special.tsx │ ├── inventory-item-tile.tsx │ ├── inventory-item-tooltip-contents.tsx │ ├── inventory-item-tooltip-exterior.tsx │ ├── inventory-item-tooltip-info.tsx │ ├── inventory-item-tooltip-name.tsx │ ├── inventory-item-tooltip-rarity.tsx │ ├── inventory-item-tooltip-seed.tsx │ ├── inventory-item-tooltip-stattrak.tsx │ ├── inventory-item-tooltip-teams.tsx │ ├── inventory-item-tooltip.tsx │ ├── inventory-item-wear.tsx │ ├── inventory-item.tsx │ ├── inventory-selected-item.tsx │ ├── inventory.tsx │ ├── item-browser.tsx │ ├── item-button.tsx │ ├── item-editor-name.tsx │ ├── item-editor.tsx │ ├── item-image.tsx │ ├── item-picker-desktop.tsx │ ├── item-picker-filter-desktop.tsx │ ├── item-picker-filter-icon.tsx │ ├── item-picker-filter-mobile.tsx │ ├── item-picker-mobile.tsx │ ├── item-picker.tsx │ ├── item-selector-context.tsx │ ├── language-select.tsx │ ├── logo.tsx │ ├── modal-button.tsx │ ├── modal-generic.tsx │ ├── modal.tsx │ ├── overlay.tsx │ ├── patch-picker.tsx │ ├── remove-item-patch.tsx │ ├── rename-item.tsx │ ├── rename-storage-unit.tsx │ ├── scrape-item-sticker.tsx │ ├── select.tsx │ ├── settings-label.tsx │ ├── splash.tsx │ ├── sticker-picker.tsx │ ├── swap-items-stattrak.tsx │ ├── sync-indicator.tsx │ ├── sync-warn.tsx │ ├── text-slider.tsx │ ├── tool-button.tsx │ ├── tool-input.tsx │ ├── translation-script.tsx │ ├── unlock-case-attribute.tsx │ ├── unlock-case-container-add-key.tsx │ ├── unlock-case-container-background.tsx │ ├── unlock-case-container-contents.tsx │ ├── unlock-case-container-unlocked.tsx │ ├── unlock-case-container.tsx │ ├── unlock-case-wheel-item.tsx │ ├── unlock-case-wheel-items.tsx │ ├── unlock-case-wheel.tsx │ ├── unlock-case.tsx │ ├── use-item-footer.tsx │ └── use-item-header.tsx ├── data │ ├── backgrounds.ts │ ├── languages.ts │ └── sync.ts ├── db.server.ts ├── entry.client.tsx ├── entry.server.tsx ├── env.server.ts ├── globals.ts ├── http.server.ts ├── logo.server.ts ├── middlewares │ ├── is-valid-api-request.server.ts │ ├── remove-trailing-dots.server.ts │ └── remove-trailing-slashes.server.ts ├── models │ ├── api-auth-token.server.ts │ ├── api-credential.server.ts │ ├── rule.server.ts │ ├── rule.ts │ ├── user-cache.server.ts │ ├── user-preference.server.ts │ └── user.server.ts ├── preferences │ ├── background.server.ts │ ├── language.server.ts │ └── toggleable.server.ts ├── responses.server.ts ├── root-meta.ts ├── root-seo.ts ├── root.tsx ├── routes.ts ├── routes │ ├── $.tsx │ ├── _index.tsx │ ├── api.$.tsx │ ├── api.action.preferences._index.tsx │ ├── api.action.resync._index.tsx │ ├── api.action.sync._index.tsx │ ├── api.action.unlock-case._index.tsx │ ├── api.add-container._index.tsx │ ├── api.add-item._index.tsx │ ├── api.equipped.v3.$userId[.]json._index.tsx │ ├── api.increment-item-stattrak._index.tsx │ ├── api.inventory.$userId[.]json._index.tsx │ ├── api.sign-in._index.tsx │ ├── api.sign-in.callback._index.tsx │ ├── api.user.$userId._index.tsx │ ├── api.users._index.tsx │ ├── app[.]webmanifest.tsx │ ├── craft._index.tsx │ ├── index[.]html._index.tsx │ ├── settings._index.tsx │ ├── sign-in._index.tsx │ ├── sign-in.steam.callback._index.tsx │ ├── sign-out._index.tsx │ └── translations.$language[.]js._index.tsx ├── routines │ └── setup-purge.ts ├── session.server.ts ├── singleton.server.ts ├── steam-strategy.server.ts ├── sync.ts ├── tailwind.css ├── translation.server.ts ├── translations │ ├── brazilian.ts │ ├── bulgarian.ts │ ├── czech.ts │ ├── danish.ts │ ├── dutch.ts │ ├── english.ts │ ├── finnish.ts │ ├── french.ts │ ├── german.ts │ ├── greek.ts │ ├── hungarian.ts │ ├── index.ts │ ├── indonesian.ts │ ├── italian.ts │ ├── japanese.ts │ ├── koreana.ts │ ├── latam.ts │ ├── norwegian.ts │ ├── polish.ts │ ├── portuguese.ts │ ├── romanian.ts │ ├── russian.ts │ ├── schinese.ts │ ├── spanish.ts │ ├── swedish.ts │ ├── tchinese.ts │ ├── thai.ts │ ├── turkish.ts │ ├── ukrainian.ts │ └── vietnamese.ts └── utils │ ├── economy-filters.ts │ ├── economy.ts │ ├── fetch.ts │ ├── inventory-cached-data.ts │ ├── inventory-equipped-v3.ts │ ├── inventory-filters.ts │ ├── inventory-transform.ts │ ├── inventory.ts │ ├── localstorage.ts │ ├── misc.ts │ ├── number.ts │ ├── promise.ts │ ├── shapes.server.ts │ ├── shapes.ts │ ├── sound.ts │ ├── splash.ts │ ├── translation.ts │ └── user-cached-data.ts ├── docs ├── api.md ├── api.sh ├── rules.md └── self-hosting.md ├── env.d.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma ├── migrations │ ├── 20231029160427_postgres │ │ └── migration.sql │ ├── 20231111181243_add_user_preferences │ │ └── migration.sql │ ├── 20231219002922_add_background_column │ │ └── migration.sql │ ├── 20231231144047_stats_for_nerds_setting │ │ └── migration.sql │ ├── 20231231164103_hide_free_items_setting │ │ └── migration.sql │ ├── 20240103010021_create_api_credential_model │ │ └── migration.sql │ ├── 20240224204138_add_api_auth_token │ │ └── migration.sql │ ├── 20240224232528_add_relation_to_api_key │ │ └── migration.sql │ ├── 20240319215237_add_rule_table │ │ └── migration.sql │ ├── 20240320002722_add_rule_overwrite_table │ │ └── migration.sql │ ├── 20240325225022_rename_user_preferences │ │ └── migration.sql │ ├── 20240330001552_add_hide_filters_preference │ │ └── migration.sql │ ├── 20240405161646_add_scope_column │ │ └── migration.sql │ ├── 20240405202805_add_comment_column │ │ └── migration.sql │ ├── 20240406232553_rename_ruleoverwrite_table │ │ └── migration.sql │ ├── 20240406233816_add_group_tables │ │ └── migration.sql │ ├── 20240406235753_on_delete_cascade │ │ └── migration.sql │ ├── 20240408144640_add_args_column │ │ └── migration.sql │ ├── 20240408174019_lowercase_rules │ │ └── migration.sql │ ├── 20240423224735_add_domain │ │ └── migration.sql │ ├── 20240423230411_image_optional │ │ └── migration.sql │ ├── 20240423230612_add_default_domain │ │ └── migration.sql │ ├── 20240423232015_add_domain_id │ │ └── migration.sql │ ├── 20240424131651_rollback_domain_stuff │ │ └── migration.sql │ ├── 20240424172222_add_domain │ │ └── migration.sql │ ├── 20240424172308_add_image │ │ └── migration.sql │ ├── 20240424175224_add_user_domain_inventory │ │ └── migration.sql │ ├── 20240424175939_add_synced_at │ │ └── migration.sql │ ├── 20240424180112_remove_domain_id │ │ └── migration.sql │ ├── 20240622012703_drop_domains_table │ │ └── migration.sql │ ├── 20240702222340_remove_inventories │ │ └── migration.sql │ ├── 20250404001916_add_hide_new_item_label_preference │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── favicon.ico ├── images │ ├── flags │ ├── inventory-simulator.png │ ├── inventory-simulator.svg │ ├── stattrak-module.png │ ├── thumbnails │ │ ├── 100.png │ │ ├── 1024.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 128.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 16.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 192.png │ │ ├── 20.png │ │ ├── 256.png │ │ ├── 29.png │ │ ├── 32.png │ │ ├── 40.png │ │ ├── 50.png │ │ ├── 512.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 64.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ └── 87.png │ └── vectors │ │ ├── ct.svg │ │ └── t.svg ├── scripts │ └── service-worker.js ├── sounds │ ├── buttonclick.wav │ ├── case_awarded_ancient.wav │ ├── case_awarded_common.wav │ ├── case_awarded_legendary.wav │ ├── case_awarded_mythical.wav │ ├── case_awarded_rare.wav │ ├── case_awarded_uncommon.wav │ ├── case_drop.wav │ ├── case_scroll.wav │ ├── case_unlock.wav │ ├── inventory_item_close.wav │ ├── inventory_item_pickup.wav │ ├── inventory_item_putdown.wav │ ├── inventory_new_item_accept.wav │ ├── music_equip.wav │ ├── roll.mp3 │ ├── sticker_apply.wav │ ├── sticker_apply_confirm.wav │ ├── sticker_scratch1.wav │ ├── sticker_scratch2.wav │ ├── sticker_scratch3.wav │ ├── sticker_scratch4.wav │ └── sticker_scratch5.wav └── videos │ ├── bg-ancient.webm │ ├── bg-anubis.webm │ ├── bg-apollo.webm │ ├── bg-blacksite.webm │ ├── bg-cbble.webm │ ├── bg-chlorine.webm │ ├── bg-county.webm │ ├── bg-engage.webm │ ├── bg-guard.webm │ ├── bg-mutiny.webm │ ├── bg-nuke.webm │ ├── bg-search.webm │ ├── bg-sirocco.webm │ ├── bg-sirocco_night.webm │ ├── bg-swamp.webm │ └── bg-vertigo.webm ├── react-router.config.ts ├── screenshot1.png ├── screenshot2.png ├── scripts ├── assert-cslib-v5-migration.ts ├── postinstall.ts └── translate.ts ├── start.sh ├── tsconfig.json ├── vite.config.ts └── vitest.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | # Required environment variables 2 | DATABASE_URL="postgres://user:pwd@host:port/dbname?schema=public" 3 | SESSION_SECRET="your session secret" 4 | 5 | # Optional environment variables 6 | # Initial value for `steamApiKey` rule. 7 | STEAM_API_KEY="the key from https://steamcommunity.com/dev/apikey" 8 | # Initial value for `steamCallbackUrl` rule. 9 | STEAM_CALLBACK_URL="http://your.domain/sign-in/steam/callback" 10 | # Cloudflare Analytics token 11 | CLOUDFLARE_ANALYTICS_TOKEN="your cloudflare web analytics token" 12 | # Only change this if you're handling assets hosting on your own. 13 | # You may remove it if you're using our default CDN. 14 | # This will NOT affect items that uses Valve's CDN (mostly stickers and items from CS:GO). 15 | ASSETS_BASE_URL="" -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://www.paypal.com/donate/?hosted_button_id=KKE7AT623ALX2"] 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - "docs/**" 9 | pull_request: 10 | branches: 11 | - main 12 | paths-ignore: 13 | - "docs/**" 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | node-version: [20.x] 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - name: Install dependencies 29 | run: npm ci 30 | - name: Typecheck code 31 | run: npm run typecheck 32 | - name: Run tests 33 | run: npm run test 34 | - name: Build application 35 | run: npm run build 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build-last-commit 2 | .env 3 | .react-router/ 4 | /.cache 5 | /build 6 | /prisma/dev.db 7 | /prisma/dev.db-journal 8 | /public/build 9 | node_modules -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | build 3 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"], 3 | "printWidth": 80, 4 | "proseWrap": "never", 5 | "semi": true, 6 | "singleQuote": false, 7 | "tabWidth": 2, 8 | "trailingComma": "none" 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS development-dependencies-env 2 | COPY . /app 3 | WORKDIR /app 4 | RUN npm ci 5 | 6 | FROM node:20-alpine AS production-dependencies-env 7 | COPY ./package.json package-lock.json prisma /app/ 8 | WORKDIR /app 9 | RUN npm ci --omit=dev && npx prisma generate 10 | 11 | FROM node:20-alpine AS build-env 12 | COPY . /app/ 13 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 14 | WORKDIR /app 15 | RUN npm run build 16 | 17 | FROM node:20-alpine 18 | COPY ./package.json package-lock.json start.sh prisma /app/ 19 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 20 | COPY --from=build-env /app/build /app/build 21 | WORKDIR /app 22 | ENTRYPOINT ["./start.sh"] 23 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 - present Ian Lucas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /app/api-strategy.server.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Ian Lucas. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { fail } from "@ianlucas/cs2-lib"; 7 | import { Strategy } from "remix-auth/strategy"; 8 | import SteamAPI, { UserSummary } from "steamapi"; 9 | import { z } from "zod"; 10 | import { 11 | clearAuthTokens, 12 | clearExpiredAuthTokens, 13 | getAuthTokenDetails 14 | } from "./models/api-auth-token.server"; 15 | import { steamApiKey } from "./models/rule.server"; 16 | import { upsertUser } from "./models/user.server"; 17 | 18 | namespace ApiStrategy { 19 | export type VerifyOptions = { 20 | request: Request; 21 | userId: string; 22 | }; 23 | } 24 | 25 | export class ApiStrategy extends Strategy { 26 | name = "api"; 27 | 28 | constructor() { 29 | super( 30 | async ({ userId }) => 31 | await upsertUser( 32 | (await new SteamAPI(await steamApiKey.get()).getUserSummary( 33 | userId 34 | )) as UserSummary 35 | ) 36 | ); 37 | } 38 | 39 | async authenticate(request: Request) { 40 | const url = new URL(request.url); 41 | const token = z.string().parse(url.searchParams.get("token")); 42 | const { exists, userId, valid } = await getAuthTokenDetails(token); 43 | 44 | if (!exists) { 45 | fail("Invalid token."); 46 | } 47 | 48 | if (!valid) { 49 | await clearExpiredAuthTokens(userId); 50 | fail("Expired token."); 51 | } 52 | 53 | await clearAuthTokens(userId); 54 | 55 | return await this.verify({ userId, request }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/api.server.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Ian Lucas. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export function api(action: (args: T) => Promise) { 7 | return async function handler(args: T): Promise { 8 | try { 9 | return await action(args); 10 | } catch (error) { 11 | if (error instanceof Response) { 12 | throw error; 13 | } 14 | let errorMessage = "Internal server error"; 15 | let logError = true; 16 | let statusCode = 500; 17 | if (error instanceof Error) { 18 | if (error.name === "ZodError") { 19 | errorMessage = 20 | "Please check this endpoint's documentation for the correct request parameters."; 21 | logError = false; 22 | statusCode = 400; 23 | } 24 | } 25 | if (logError) { 26 | console.error(error); 27 | } 28 | throw new Response(JSON.stringify({ error: errorMessage }), { 29 | status: statusCode, 30 | headers: { 31 | "Content-Type": "application/json" 32 | } 33 | }); 34 | } 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /app/app-defaults.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Ian Lucas. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export const DEFAULT_APP_NAME = "CS2 Inventory Simulator"; 7 | export const DEFAULT_APP_FOOTER_NAME = "cstrike's Inventory Simulator"; 8 | export const DEFAULT_APP_DESCRIPTION = `Counter-Strike 2 Inventory Simulator. Craft items, open cases, and apply stickers - organize and plan your dream inventory.`; 9 | export const DEFAULT_APP_TITLE = `The best free, Counter-Strike 2 Inventory Simulator`; 10 | export const DEFAULT_APP_IMAGE = "/images/inventory-simulator.png"; 11 | -------------------------------------------------------------------------------- /app/app.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Ian Lucas. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export interface UnlockCaseEventData { 7 | caseUid: number; 8 | keyUid?: number; 9 | } 10 | 11 | export const app = new EventTarget(); 12 | 13 | export function dispatchAppEvent( 14 | name: "unlockcase", 15 | payload: UnlockCaseEventData 16 | ): void; 17 | export function dispatchAppEvent(name: string, payload: any) { 18 | app.dispatchEvent(new CustomEvent(name, { detail: payload })); 19 | } 20 | -------------------------------------------------------------------------------- /app/auth.server.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Ian Lucas. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { redirect } from "react-router"; 7 | import { Authenticator } from "remix-auth"; 8 | import { getSession } from "~/session.server"; 9 | import { ApiStrategy } from "./api-strategy.server"; 10 | import { findUniqueUser } from "./models/user.server"; 11 | import { SteamStrategy } from "./steam-strategy.server"; 12 | 13 | export const authenticator = new Authenticator(); 14 | 15 | authenticator.use(new SteamStrategy(), "steam").use(new ApiStrategy(), "api"); 16 | 17 | export async function getRequestUserId(request: Request) { 18 | const session = await getSession(request.headers.get("cookie")); 19 | const userId = session.get("userId") as unknown; 20 | if (typeof userId !== "string") { 21 | return undefined; 22 | } 23 | return userId; 24 | } 25 | 26 | export async function findRequestUser(request: Request) { 27 | try { 28 | const userId = await getRequestUserId(request); 29 | if (userId === undefined) { 30 | return undefined; 31 | } 32 | return await findUniqueUser(userId); 33 | } catch { 34 | throw redirect("/sign-out"); 35 | } 36 | } 37 | 38 | export async function requireUser(request: Request) { 39 | const user = await findRequestUser(request); 40 | if (user === undefined) { 41 | throw redirect("/sign-in"); 42 | } 43 | return user; 44 | } 45 | -------------------------------------------------------------------------------- /app/components/background.tsx: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Ian Lucas. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { useMemo } from "react"; 7 | import { backgrounds } from "~/data/backgrounds"; 8 | import { random } from "~/utils/misc"; 9 | import { usePreferences } from "./app-context"; 10 | 11 | export function Background() { 12 | const { background: current } = usePreferences(); 13 | 14 | const background = useMemo(() => { 15 | return current ?? random(backgrounds).value; 16 | }, [current]); 17 | 18 | return ( 19 |