diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d151889..6dab392 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,27 +2,37 @@ name: Lora CI on: push: - branches: [ main, master, develop ] + branches: [main, master, develop] pull_request: - branches: [ main, master, develop ] + branches: [main, master, develop] + workflow_dispatch: + inputs: + environment: + description: "Environment to deploy to" + required: true + default: "production" + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} jobs: test: name: Build and Test runs-on: ubuntu-latest - + env: MIX_ENV: test - + steps: - uses: actions/checkout@v3 - + - name: Set up Elixir uses: erlef/setup-beam@v1 with: elixir-version: "1.18.2" # [Required] Define the Elixir version otp-version: "27.2.1" # [Required] Define the Erlang/OTP version - + - name: Restore dependencies cache uses: actions/cache@v4 with: @@ -36,20 +46,19 @@ jobs: mix local.rebar --force mix local.hex --force mix deps.get - + - name: Run formatter check run: mix format --check-formatted - + - name: Compile (with warnings as errors) run: mix compile --warnings-as-errors - - # - name: Run Dialyzer - # run: mix dialyzer - + + # - name: Run Dialyzer + # run: mix dialyzer + - name: Run tests with coverage run: mix test.with_coverage - - name: Archive code coverage results uses: actions/upload-artifact@v4 with: @@ -57,4 +66,158 @@ jobs: path: | cover/ retention-days: 21 - + + dockerize: + name: Build and Publish Docker image + needs: [test] + runs-on: ubuntu-latest + # if: github.event.pull_request.merged == true + permissions: + contents: read + packages: write + outputs: + image_tag: ${{ steps.save-image-tag.outputs.image_tag }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Generate version + id: version + run: | + SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-8) + TIMESTAMP=$(date +%Y%m%d%H%M%S) + echo "docker_version=${TIMESTAMP}-${SHORT_SHA}" >> $GITHUB_OUTPUT + + # Only login to registry if we're on main branch + - name: Log in to GitHub Container Registry + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + # Only push if we're on main branch + push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.docker_version }} + ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && format('{0}/{1}:latest', env.REGISTRY, env.IMAGE_NAME) || '' }} + labels: | + org.opencontainers.image.version=${{ steps.version.outputs.docker_version }} + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + build-args: | + GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTOR=${{ github.actor }} + GITHUB_REPOSITORY_OWNER=${{ github.repository_owner }} + PROJECTS_URL=${{ vars.PROJECTS_URL }} + ACCOUNTS_URL=${{ vars.ACCOUNTS_URL }} + # Save image tag for deployment workflow + - name: Save build info + id: save-image-tag + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + IMAGE_TAG="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.docker_version }}" + echo "$IMAGE_TAG" > build-info.txt + echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT + - name: Upload build info + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 + with: + name: build-info + path: build-info.txt + retention-days: 7 + + comment-pr: + name: Comment Pull Request + needs: [dockerize] + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + permissions: + pull-requests: write + packages: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Comment PR + run: | + gh pr comment ${{ github.event.pull_request.number }} --body "πŸš€ Docker image built successfully with tag: \`${{ needs.setup-version.outputs.docker_version }}\` + + To test this image locally: + \`\`\`bash + docker pull ${{ env.REGISTRY }}/${{ github.repository }}:${{ needs.setup-version.outputs.docker_version }} + docker run --rm -p 4000:4000 -e SECRET_KEY_BASE=\$(openssl rand -base64 48) ${{ env.REGISTRY }}/${{ github.repository }}:${{ needs.setup-version.outputs.docker_version }} + \`\`\`" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + deploy: + name: Deploy to Production + needs: [dockerize] + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') + environment: + name: ${{ github.event.inputs.environment || 'production' }} + url: ${{ vars.DEPLOYMENT_URL }} + # Manual approval required for deployment + concurrency: + group: ${{ github.event.inputs.environment || 'production' }}_environment + permissions: + contents: read + packages: read + + steps: + - name: Download build info + uses: actions/download-artifact@v4 + with: + name: build-info + + - name: Set image tag + id: build-info + run: | + IMAGE_TAG=$(cat build-info.txt) + echo "Using image tag: $IMAGE_TAG" + echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT + + - name: Set up SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + echo "${{ secrets.DEPLOY_SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts + chmod 644 ~/.ssh/known_hosts + + - name: Deploy via SSH + env: + IMAGE_TAG: ${{ steps.build-info.outputs.image_tag }} + DEPLOY_SERVER: ${{ secrets.DEPLOY_SERVER }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} + run: | + # SSH to the server and update the docker-compose.yml file with the new image tag + ssh $DEPLOY_USER@$DEPLOY_SERVER "cd $DEPLOY_PATH && \ + export IMAGE_TAG=$IMAGE_TAG && \ + sed -i 's|image:.*lora:.*|image: $IMAGE_TAG|' docker-compose.yml && \ + docker compose pull && \ + docker compose up -d" + + - name: Verify deployment + env: + DEPLOY_SERVER: ${{ secrets.DEPLOY_SERVER }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} + run: | + ssh $DEPLOY_USER@$DEPLOY_SERVER "cd $DEPLOY_PATH && \ + docker compose ps && \ + echo 'Deployment completed successfully!'" diff --git a/Dockerfile b/Dockerfile index 88046f7..340039d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,8 @@ RUN apt-get update && \ # Set environment variables ENV MIX_ENV=prod \ - LANG=C.UTF-8 + LANG=C.UTF-8 \ + ELIXIR_VERSION=1.17.0 # Install hex and rebar RUN mix local.hex --force && \ @@ -34,7 +35,6 @@ COPY . . # Build and digest assets RUN cd apps/lora_web/assets && \ npm ci --progress=false --no-audit --loglevel=error && \ - npm run deploy && \ cd ../../.. && \ mix assets.deploy @@ -42,33 +42,34 @@ RUN cd apps/lora_web/assets && \ RUN mix do compile, release # Release stage -FROM debian:bullseye-slim AS app +FROM debian:bookworm-slim AS app RUN apt-get update && \ - apt-get install -y openssl libncurses5 locales && \ + apt-get install -y openssl libncurses5 locales ca-certificates && \ apt-get clean && rm -rf /var/lib/apt/lists/* # Set locale RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US:en -ENV LC_ALL en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 # Create a non-root user and group RUN groupadd --gid 1000 lora && \ useradd --uid 1000 --gid lora --shell /bin/bash --create-home lora WORKDIR /app -COPY --from=builder /app/_build/prod/rel/lora ./ +COPY --from=builder --chown=lora:lora /app/_build/prod/rel/lora ./ # Set ownership to non-root user -RUN chown -R lora:lora /app +# RUN chown -R lora:lora /app USER lora EXPOSE 4000 -ENV RELEASE_NODE=lora@127.0.0.1 -ENV PHX_SERVER=true -ENV PHX_HOST=localhost +ENV RELEASE_DISTRIBUTION=sname \ + RELEASE_NODE=lora \ + PHX_SERVER=true \ + PHX_HOST=localhost CMD ["bin/lora", "start"] diff --git a/REQUIREMENTS-002.md b/REQUIREMENTS-002.md index cb5c911..32243db 100644 --- a/REQUIREMENTS-002.md +++ b/REQUIREMENTS-002.md @@ -1,5 +1,120 @@ -# Phase 2 Software Reuqirements Sepcification +# Lora Game – Authentication and Enhanced Lobby SRS (v 0.2-draft) -## Scope -This change should not include any function change, only cosmetic +## 1. Scope +Add user authentication and improve lobby flow for the existing **Lora** game MVP. Authentication relies on **Ueberauth + Auth0** with no durable persistence. Anonymous visitors may browse the lobby list, but **must authenticate** before creating or joining a game. +## 2. Definitions +| Term | Meaning | +| --- | --- | +| **Anonymous Visitor** | Unauthenticated user browsing the lobby page. | +| **Authenticated Player** | User logged in through Auth0 and cached in ETS. | +| **State Param** | OAuth2 `state` value that carries the **target redirect path** (e.g. `/game/XYZ123`) through the Auth0 round-trip. | +| **Callback URL** | `/auth/auth0/callback` handled by Ueberauth after Auth0 login. | + +## 3. Actors +- **Visitor** – accesses the lobby without logging in. +- **Player** – authenticated user who can create or join a game. +- **Auth0** – external identity provider (Universal Login). +- **System** – Phoenix app with Ueberauth plug chain and in-memory session store. + +## 4. Functional Requirements + +### 4.1 Authentication +| ID | Requirement | +| --- | --- | +| **AUTH-A-01** | Integrate `ueberauth` and `ueberauth_auth0`. | +| **AUTH-A-02** | `/auth/auth0` initiates login; `/auth/auth0/callback` processes Auth0 response. | +| **AUTH-A-03** | Use Auth0 Universal Login (redirect flow, no embedded widget). | +| **AUTH-A-04** | On successful callback, extract `sub`, `name`, `email` into `%Player{}`. | +| **AUTH-A-05** | Store `%Player{}` in ETS via `Lora.Accounts` context facade. | +| **AUTH-A-06** | Maintain Phoenix session cookie with `player_id` referencing ETS record. | +| **AUTH-A-07** | Provide `/logout` route that clears session and purges ETS entry. | + +### 4.2 Lobby Flow +| ID | Requirement | +| --- | --- | +| **AUTH-L-01** | `/lobby` lists open game codes and a **Create Game** button (visible to all). | +| **AUTH-L-02** | *Join* or *Create* triggers an auth guard; unauthenticated users are redirected to `/auth/auth0?state=`. | +| **AUTH-L-03** | After callback the app reads `conn.query_params["state"]` (decoded) to decide next redirect. | +| **AUTH-L-04** | If `state` is `/lobby#create`, create a fresh game and redirect to its URL. | +| **AUTH-L-05** | If `state` is `/game/`, attempt to join; on success redirect there, else flash error and return to lobby. | +| **AUTH-L-06** | Authenticated user’s name appears in lobby header with logout dropdown. | + +### 4.3 Security & Session +| ID | Requirement | +| --- | --- | +| **AUTH-S-01** | Use encrypted Phoenix session cookies (`encrypt: true`). | +| **AUTH-S-02** | ETS player entries expire 30 min after last touch via periodic sweep. | +| **AUTH-S-03** | Ensure CSRF protection on `/auth/auth0/callback` (Ueberauth default). | + +## 5. Non-Functional Requirements +| ID | Requirement | +| --- | --- | +| **AUTH-NFR-P-01** | Use `ueberauth` β‰₯ 0.11 and `ueberauth_auth0` β‰₯ 0.9. | +| **AUTH-NFR-P-02** | Hide persistence behind `Lora.Accounts` behaviour so future adapters (Ecto, microservice) can be swapped without touching controllers or LiveViews. | +| **AUTH-NFR-P-03** | Typical added latency for auth round-trip **< 1500 ms**. | +| **AUTH-NFR-Q-01** | ExUnit tests mocking Auth0 responses: success, invalid state, expired session, join failure. | + +## 6. Architecture Changes +- **Plug Pipeline**: `:browser` now includes `Ueberauth` and `LoraWeb.Plugs.RequireAuth`. +- **Context**: `Lora.Accounts` provides `get_player/1`, `store_player/1`, `delete_player/1` with ETS adapter. +- **LiveViews**: `LobbyLive` handles auth redirects; `GameLive` mounts only when `current_player` assigned. +- **ETS Table**: `:players`, key = Auth0 `sub`, value = `%Player{}` plus `inserted_at` timestamp. + +## 7. Auth0 Configuration + +**Allowed Callback URLs** +``` +https://your-host/auth/auth0/callback +http://localhost:4000/auth/auth0/callback +``` + +**Allowed Logout URLs** +https://your-host/ +http://localhost:4000/ + +**State Parameter examples (URL-encoded)** +``` +state=%2Flobby%23create # create a new game after login +state=%2Fgame%2FXYZ123 # join existing game /game/XYZ123 +``` + +## 8. Sequence Diagram (textual) +1. Visitor clicks **Join game XYZ123**. +2. `LobbyLive` pushes redirect to `/auth/auth0?state=%2Fgame%2FXYZ123`. +3. Browser loads Auth0 Universal Login. +4. User authenticates. +5. Auth0 redirects to `/auth/auth0/callback?code=…&state=%2Fgame%2FXYZ123`. +6. Ueberauth exchanges code and obtains profile. +7. Controller stores player in ETS, sets session `player_id`. +8. Controller redirects user to `/game/XYZ123`. + + +```mermaid +sequenceDiagram + participant V as Visitor (Browser) + participant LL as LobbyLive + participant Auth0 as Auth0 + participant C as Callback Controller + participant ETS as ETS Store + V->>LL: Click Join /game/XYZ123 + LL-->>V: 302 to /auth/auth0?state=/game/XYZ123 + V->>Auth0: GET Universal Login + Auth0-->>V: Login Form + V->>Auth0: Credentials + Auth0-->>V: 302 to /auth/auth0/callback?code=...&state=/game/XYZ123 + V->>C: GET callback with code & state + C->>Auth0: Exchange code for tokens + Auth0-->>C: ID token & profile + C->>ETS: store_player(profile) + C-->>V: 302 to /game/XYZ123 (set session cookie) + V->>GameLive: WebSocket join /game/XYZ123 +``` + +## 9. Deliverables +1. Updated Mix dependencies (`ueberauth`, `ueberauth_auth0`). +2. `Lora.Accounts` context with ETS adapter and behaviour spec. +3. Router updates for auth routes. +4. `LoraWeb.Plugs.RequireAuth`. +5. Modified `LobbyLive` and templates. +6. Test suite for auth flow. \ No newline at end of file diff --git a/apps/lora/lib/lora.ex b/apps/lora/lib/lora.ex index 1dda9d1..d471d9e 100644 --- a/apps/lora/lib/lora.ex +++ b/apps/lora/lib/lora.ex @@ -81,4 +81,88 @@ defmodule Lora do def generate_player_id do :crypto.strong_rand_bytes(16) |> Base.encode16(case: :lower) end + + @doc """ + Lists all open games that are waiting for players to join. + + Returns a list of game structs with basic information. + """ + def list_open_games do + # Get all active game IDs from supervisor + game_ids = GameSupervisor.list_games() + + # Fetch the state of each game and filter for those that are still accepting players + game_ids + |> Enum.map(fn id -> + case get_game_state(id) do + {:ok, game} -> {id, game} + _ -> nil + end + end) + |> Enum.filter(&(&1 != nil)) + |> Enum.filter(fn {_id, game} -> + # Games are "open" if they are in lobby phase + # and have fewer than 4 players + game.phase == :lobby and length(game.players) < 4 + end) + |> Enum.map(fn {id, game} -> + first_player = List.first(game.players) + creator_name = if first_player, do: first_player.name, else: "Unknown" + + %{ + id: id, + players: Enum.map(game.players, & &1.name), + player_count: length(game.players), + created_at: Map.get(game, :created_at, DateTime.utc_now()), + creator: creator_name + } + end) + end + + @doc """ + Lists all games that a player is actively participating in. + + Returns a list of game structs with basic information. + """ + def list_player_active_games(player_id) do + # Get all active game IDs from supervisor + game_ids = GameSupervisor.list_games() + + # Fetch the state of each game and filter for those containing the player + game_ids + |> Enum.map(fn id -> + case get_game_state(id) do + {:ok, game} -> {id, game} + _ -> nil + end + end) + |> Enum.filter(&(&1 != nil)) + |> Enum.filter(fn {_id, game} -> + # Check if player is in this game + Enum.any?(game.players, fn p -> p.id == player_id end) + end) + |> Enum.map(fn {id, game} -> + player = Enum.find(game.players, fn p -> p.id == player_id end) + + opponent_names = + game.players + |> Enum.reject(fn p -> p.id == player_id end) + |> Enum.map(& &1.name) + + %{ + id: id, + players: Enum.map(game.players, & &1.name), + player_count: length(game.players), + playing: game.phase != :lobby, + last_activity: + Map.get(game, :last_activity, Map.get(game, :created_at, DateTime.utc_now())), + your_turn: + Map.get(game, :current_player_idx, nil) == + Enum.find_index(game.players, fn p -> p.id == player_id end), + your_cards: Map.get(player, :hand, []), + opponents: opponent_names, + created_at: Map.get(game, :created_at, DateTime.utc_now()) + } + end) + end end diff --git a/apps/lora/lib/lora/accounts.ex b/apps/lora/lib/lora/accounts.ex new file mode 100644 index 0000000..e55c9e2 --- /dev/null +++ b/apps/lora/lib/lora/accounts.ex @@ -0,0 +1,53 @@ +defmodule Lora.Accounts do + @moduledoc """ + The Accounts context responsible for player authentication and management. + """ + + alias Lora.Accounts.Player + + # Use the ETS adapter as the default implementation + @adapter Lora.Accounts.ETSAdapter + + @doc """ + Initialize the accounts system. + """ + def init do + @adapter.init() + end + + @doc """ + Get a player by ID. + """ + def get_player(player_id) do + @adapter.get_player(player_id) + end + + @doc """ + Store a player. + """ + def store_player(%Player{} = player) do + @adapter.store_player(player) + end + + @doc """ + Delete a player. + """ + def delete_player(player_id) do + @adapter.delete_player(player_id) + end + + @doc """ + Update the timestamp for a player to prevent session expiry. + """ + def touch_player(player_id) do + @adapter.touch_player(player_id) + end + + @doc """ + Create a player from Auth0 authentication information. + """ + def create_player_from_auth(%Ueberauth.Auth{} = auth) do + player = Player.from_auth(auth) + store_player(player) + end +end diff --git a/apps/lora/lib/lora/accounts/accounts_behaviour.ex b/apps/lora/lib/lora/accounts/accounts_behaviour.ex new file mode 100644 index 0000000..5d2e761 --- /dev/null +++ b/apps/lora/lib/lora/accounts/accounts_behaviour.ex @@ -0,0 +1,11 @@ +defmodule Lora.Accounts.AccountsBehaviour do + @moduledoc """ + Behaviour for the Accounts context to allow swapping implementations. + """ + alias Lora.Accounts.Player + + @callback get_player(player_id :: String.t()) :: {:ok, Player.t()} | {:error, String.t()} + @callback store_player(player :: Player.t()) :: {:ok, Player.t()} | {:error, String.t()} + @callback delete_player(player_id :: String.t()) :: :ok | {:error, String.t()} + @callback touch_player(player_id :: String.t()) :: :ok | {:error, String.t()} +end diff --git a/apps/lora/lib/lora/accounts/ets_adapter.ex b/apps/lora/lib/lora/accounts/ets_adapter.ex new file mode 100644 index 0000000..9fb5f5f --- /dev/null +++ b/apps/lora/lib/lora/accounts/ets_adapter.ex @@ -0,0 +1,96 @@ +defmodule Lora.Accounts.ETSAdapter do + @moduledoc """ + ETS-based implementation of the Accounts context. + """ + @behaviour Lora.Accounts.AccountsBehaviour + + alias Lora.Accounts.Player + require Logger + + @table_name :players + # 30 minutes + @expiry_time_seconds 30 * 60 + + @doc """ + Initialize the ETS table for players. + """ + def init do + case :ets.whereis(@table_name) do + :undefined -> + :ets.new(@table_name, [:set, :public, :named_table]) + schedule_cleanup() + :ok + + _ -> + # Table already exists + :ok + end + end + + @impl true + def get_player(player_id) when is_binary(player_id) do + case :ets.lookup(@table_name, player_id) do + [{^player_id, player}] -> + {:ok, player} + + [] -> + {:error, "Player not found"} + end + end + + @impl true + def store_player(%Player{} = player) do + true = :ets.insert(@table_name, {player.sub, player}) + {:ok, player} + end + + @impl true + def delete_player(player_id) when is_binary(player_id) do + true = :ets.delete(@table_name, player_id) + :ok + end + + @impl true + def touch_player(player_id) when is_binary(player_id) do + case get_player(player_id) do + {:ok, player} -> + player = %Player{player | inserted_at: DateTime.utc_now()} + store_player(player) + :ok + + error -> + error + end + end + + # Schedule a periodic cleanup of expired sessions + defp schedule_cleanup do + # Since we're not a GenServer, we'll use a Task instead + Task.start(fn -> + # Wait 1 minute + Process.sleep(60_000) + cleanup_expired_players() + schedule_cleanup() + end) + end + + # Clean up expired player entries + defp cleanup_expired_players do + now = DateTime.utc_now() + expiry_threshold = DateTime.add(now, -@expiry_time_seconds, :second) + + # Perform the cleanup by iterating through the table + :ets.foldl( + fn {id, player}, acc -> + if DateTime.compare(player.inserted_at, expiry_threshold) == :lt do + Logger.info("Removing expired player session for #{player.name}") + :ets.delete(@table_name, id) + end + + acc + end, + nil, + @table_name + ) + end +end diff --git a/apps/lora/lib/lora/accounts/player.ex b/apps/lora/lib/lora/accounts/player.ex new file mode 100644 index 0000000..1e1d6f2 --- /dev/null +++ b/apps/lora/lib/lora/accounts/player.ex @@ -0,0 +1,40 @@ +defmodule Lora.Accounts.Player do + @moduledoc """ + Schema for a player in the system. + """ + + @type t :: %__MODULE__{ + id: String.t(), + sub: String.t(), + name: String.t(), + email: String.t(), + inserted_at: DateTime.t() + } + + @derive {Jason.Encoder, only: [:id, :name, :email]} + defstruct [:id, :sub, :name, :email, :inserted_at] + + @doc """ + Create a player from Auth0 information. + """ + def from_auth(%Ueberauth.Auth{} = auth) do + %__MODULE__{ + id: auth.uid, + sub: auth.uid, + name: get_name_from_auth(auth), + email: get_email_from_auth(auth), + inserted_at: DateTime.utc_now() + } + end + + defp get_name_from_auth(%{info: %{name: name}}) when not is_nil(name), do: name + defp get_name_from_auth(%{info: %{nickname: nickname}}) when not is_nil(nickname), do: nickname + + defp get_name_from_auth(%{info: %{first_name: first_name}}) when not is_nil(first_name), + do: first_name + + defp get_name_from_auth(_), do: "Anonymous Player" + + defp get_email_from_auth(%{info: %{email: email}}) when not is_nil(email), do: email + defp get_email_from_auth(_), do: nil +end diff --git a/apps/lora/lib/lora/application.ex b/apps/lora/lib/lora/application.ex index ad50513..bf9eae2 100644 --- a/apps/lora/lib/lora/application.ex +++ b/apps/lora/lib/lora/application.ex @@ -18,6 +18,10 @@ defmodule Lora.Application do {Phoenix.PubSub, name: Lora.PubSub} ] + # Initialize the Accounts ETS table + :ok = Lora.Accounts.init() + IO.puts("Accounts ETS table initialized successfully") + # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: Lora.Supervisor] diff --git a/apps/lora/lib/lora/game_supervisor.ex b/apps/lora/lib/lora/game_supervisor.ex index 1163930..44e6479 100644 --- a/apps/lora/lib/lora/game_supervisor.ex +++ b/apps/lora/lib/lora/game_supervisor.ex @@ -16,6 +16,13 @@ defmodule Lora.GameSupervisor do DynamicSupervisor.init(strategy: :one_for_one) end + @doc """ + Lists all active game IDs by querying the registry. + """ + def list_games do + Registry.select(Lora.GameRegistry, [{{:"$1", :_, :_}, [], [:"$1"]}]) + end + @doc """ Creates a new game with a random 6-character ID and starts its server. Also adds the creator as the first player. diff --git a/apps/lora/mix.exs b/apps/lora/mix.exs index 328da22..f6d1858 100644 --- a/apps/lora/mix.exs +++ b/apps/lora/mix.exs @@ -25,7 +25,7 @@ defmodule Lora.MixProject do def application do [ mod: {Lora.Application, []}, - extra_applications: [:logger, :runtime_tools] + extra_applications: [:logger, :runtime_tools, :ueberauth] ] end @@ -42,7 +42,9 @@ defmodule Lora.MixProject do {:phoenix_pubsub, "~> 2.1"}, {:jason, "~> 1.2"}, {:swoosh, "~> 1.5"}, - {:finch, "~> 0.13"} + {:finch, "~> 0.13"}, + {:ueberauth, "~> 0.10.8"}, + {:ueberauth_auth0, "~> 2.0"} ] end diff --git a/apps/lora_web/assets/css/app.css b/apps/lora_web/assets/css/app.css index 378c8f9..0653ffb 100644 --- a/apps/lora_web/assets/css/app.css +++ b/apps/lora_web/assets/css/app.css @@ -1,5 +1,81 @@ @import "tailwindcss/base"; @import "tailwindcss/components"; @import "tailwindcss/utilities"; +@import "./card_fan.css"; /* This file is for your main application CSS */ + +/* Animation for current player - emits waves */ +.current-player { + position: relative; + overflow: visible; +} + +/* First wave */ +.current-player .current-user-wave-3 { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: inherit; + pointer-events: none; + animation: emit-waves 3s infinite linear; + animation-delay: 0s; +} + +/* Second wave */ +.current-player .current-user-wave-2 { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: inherit; + pointer-events: none; + animation: emit-waves 3s infinite linear; + animation-delay: 1s; +} + +/* Third wave - using a span element to create a third wave */ +.current-player .current-user-wave-1 { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: inherit; + pointer-events: none; + animation: emit-waves 3s infinite linear; + animation-delay: 2s; +} + +/* The waves animation */ +@keyframes emit-waves { + 0% { + transform: scale(1); + box-shadow: 0 0 10px rgba(255, 255, 255, 0.8); + opacity: 0.8; + } + 100% { + transform: scale(2); + box-shadow: 0 0 10px rgba(255, 255, 255, 0); + opacity: 0; + } +} + +/* radial gradient startting from primary content color toward predefeind table color */ +.bg-raidal-spotlight { + background: + radial-gradient( + circle at 40% 50%, + theme("colors.table") 20%, + transparent 80%, + transparent + ), + linear-gradient( + transparent, + transparent 50%, + theme("colors.table") 100% + ); +} diff --git a/apps/lora_web/assets/css/card_fan.css b/apps/lora_web/assets/css/card_fan.css new file mode 100644 index 0000000..23e780f --- /dev/null +++ b/apps/lora_web/assets/css/card_fan.css @@ -0,0 +1,171 @@ +/* Card fan effect (lepeza) styles */ +.card-fan { + position: relative; + display: flex; + justify-content: center; + min-height: 220px; + margin-bottom: 30px; + padding-top: 30px; + width: 100%; +} + +.card-fan .card-stacked { + transform-origin: bottom center; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; + position: absolute; + box-shadow: 3px 3px 12px rgba(0, 0, 0, 0.3); + + border-radius: 8px; +} + +/* Enhanced visibility for card corners */ +.card-fan .card-stacked .card-corner { + font-weight: bold; + position: absolute; + z-index: 5; + line-height: 1.2; + background-color: white; + border-radius: 4px; + padding: 3px 4px; + font-size: 120%; +} + +.card-fan .card-stacked .card-corner.top-left { + top: 6px; + left: 6px; +} + +.card-fan .card-stacked .card-corner.bottom-right { + bottom: 6px; + right: 6px; + transform: rotate(180deg); +} + +/* Improved fan spread with better center position */ +/* Now starting from -40 degrees instead of -60 degrees */ +.card-fan .card-stacked:nth-child(1) { + transform: rotate(-40deg); + left: calc(50% - 80px); + bottom: 30px; +} +.card-fan .card-stacked:nth-child(2) { + transform: rotate(-33deg); + left: calc(50% - 70px); + bottom: 32px; +} +.card-fan .card-stacked:nth-child(3) { + transform: rotate(-26deg); + left: calc(50% - 60px); + bottom: 33px; +} +.card-fan .card-stacked:nth-child(4) { + transform: rotate(-20deg); + left: calc(50% - 45px); + bottom: 34px; +} +.card-fan .card-stacked:nth-child(5) { + transform: rotate(-13deg); + left: calc(50% - 30px); + bottom: 35px; +} +.card-fan .card-stacked:nth-child(6) { + transform: rotate(-7deg); + left: calc(50% - 15px); + bottom: 35px; +} +.card-fan .card-stacked:nth-child(7) { + transform: rotate(0deg); + left: calc(50% - 5px); + bottom: 35px; +} +.card-fan .card-stacked:nth-child(8) { + transform: rotate(7deg); + left: calc(50% + 10px); + bottom: 35px; +} +.card-fan .card-stacked:nth-child(9) { + transform: rotate(13deg); + left: calc(50% + 25px); + bottom: 35px; +} +.card-fan .card-stacked:nth-child(10) { + transform: rotate(20deg); + left: calc(50% + 40px); + bottom: 34px; +} +.card-fan .card-stacked:nth-child(11) { + transform: rotate(26deg); + left: calc(50% + 55px); + bottom: 33px; +} +.card-fan .card-stacked:nth-child(12) { + transform: rotate(33deg); + left: calc(50% + 65px); + bottom: 32px; +} +.card-fan .card-stacked:nth-child(13) { + transform: rotate(40deg); + left: calc(50% + 75px); + bottom: 30px; +} + +/* Enhanced hover effect for better selection */ +.card-fan .card-stacked:hover { + /* transform: translateY(-20px) rotate(0deg) scale(1.1) !important; */ + z-index: 100 !important; + box-shadow: + 0 10px 20px -3px rgba(0, 0, 0, 0.3), + 0 6px 10px -2px rgba(0, 0, 0, 0.2); + cursor: pointer; +} + +/* Size adjustments with better padding for different sizes */ +.card-fan.size-small { + height: 180px; + margin-top: 45px; +} +.card-fan.size-medium { + height: 220px; + margin-top: 55px; +} +.card-fan.size-large { + height: 260px; + margin-top: 65px; +} + +/* Enhanced card value display */ +.card-fan .card-value { + font-weight: bold; + font-size: 1.75rem; + text-align: center; + background-color: rgba(255, 255, 255, 0.75); + padding: 4px 6px; + border-radius: 8px; + + position: relative; + z-index: 4; +} + +/* Colors for different suits */ +.card-fan .text-red { + color: #e53e3e; +} +.card-fan .text-black { + color: #2d3748; +} + +/* Positioning for specific player positions */ +.left-player .card-fan { + transform: translateX(20%); +} +.right-player .card-fan { + transform: translateX(-20%); +} +.bottom-player .card-fan { + transform: translateY(-10px); +} +.top-player .card-fan { + transform: translateY(10px); +} diff --git a/apps/lora_web/assets/js/app.js b/apps/lora_web/assets/js/app.js index d5e278a..b11b340 100644 --- a/apps/lora_web/assets/js/app.js +++ b/apps/lora_web/assets/js/app.js @@ -16,29 +16,30 @@ // // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. -import "phoenix_html" +import "phoenix_html"; // Establish Phoenix Socket and LiveView configuration. -import {Socket} from "phoenix" -import {LiveSocket} from "phoenix_live_view" -import topbar from "../vendor/topbar" +import { Socket } from "phoenix"; +import { LiveSocket } from "phoenix_live_view"; +import topbar from "../vendor/topbar"; -let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") +let csrfToken = document + .querySelector("meta[name='csrf-token']") + .getAttribute("content"); let liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, - params: {_csrf_token: csrfToken} -}) + params: { _csrf_token: csrfToken }, +}); // Show progress bar on live navigation and form submits -topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) -window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) -window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) +topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" }); +window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300)); +window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide()); // connect if there are any LiveViews on the page -liveSocket.connect() +liveSocket.connect(); // expose liveSocket on window for web console debug logs and latency simulation: // >> liveSocket.enableDebug() // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.disableLatencySim() -window.liveSocket = liveSocket - +window.liveSocket = liveSocket; diff --git a/apps/lora_web/assets/package.json b/apps/lora_web/assets/package.json index 0d75534..47ff300 100644 --- a/apps/lora_web/assets/package.json +++ b/apps/lora_web/assets/package.json @@ -1,4 +1,7 @@ { + "scripts": { + "deploy": "echo 'Assets will be built by mix assets.deploy'" + }, "devDependencies": { "daisyui": "^4.12.23" } diff --git a/apps/lora_web/assets/tailwind.config.js b/apps/lora_web/assets/tailwind.config.js index 5d4ca0e..9191b5b 100644 --- a/apps/lora_web/assets/tailwind.config.js +++ b/apps/lora_web/assets/tailwind.config.js @@ -12,7 +12,7 @@ module.exports = { "../lib/lora_web/**/*.*ex" ], daisyui: { - themes: false, // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"] + themes: true, // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"] darkTheme: "dark", // name of one of the included themes for dark mode base: true, // applies background color and foreground color for root element by default styled: true, // include daisyUI colors and design decisions for all components diff --git a/apps/lora_web/lib/lora_web.ex b/apps/lora_web/lib/lora_web.ex index 62a900b..aca64f4 100644 --- a/apps/lora_web/lib/lora_web.ex +++ b/apps/lora_web/lib/lora_web.ex @@ -55,6 +55,7 @@ defmodule LoraWeb do use Phoenix.LiveView, layout: {LoraWeb.Layouts, :app} + on_mount {LoraWeb.LiveAuth, :default} unquote(html_helpers()) end end diff --git a/apps/lora_web/lib/lora_web/components/core_components.ex b/apps/lora_web/lib/lora_web/components/core_components.ex index e64b835..cf48e32 100644 --- a/apps/lora_web/lib/lora_web/components/core_components.ex +++ b/apps/lora_web/lib/lora_web/components/core_components.ex @@ -243,8 +243,7 @@ defmodule LoraWeb.CoreComponents do + + + <% else %> +
+ + + + Please sign in to create a game +
+ <% end %> + -
- - <.input - field={f[:game_code]} - value={@game_code} - placeholder="Enter 6-character code" - required={true} - maxlength={6} - class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 uppercase" - /> + +
+
+

Join an Existing Game

+ + <%= if @current_player do %> +
+ Playing as: {@current_player.name} +
+ <% else %> +
+ + + + + Sign in to join games +
+ <% end %> + + <.form + :let={f} + id="join-game-form" + for={%{}} + as={:join_game} + phx-submit="join_game" + phx-change="validate" + > +
+ + <.input + field={f[:game_code]} + value={@game_code} + placeholder="Enter 6-character code" + required={true} + maxlength={6} + class="input input-bordered w-full uppercase" + /> +
+
+ +
+ +
-
- <.button - type="submit" - class="w-full bg-green-600 text-white py-2 px-4 rounded hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2" +
+
+ + + +
+
+

Open Games

+ + <%= if Enum.empty?(@open_games) do %> +
+ - Join Game - + + + + No open games available. Why not create one?
- + <% else %> +
+ <%= for game <- @open_games do %> +
+
+
+

+ Game: {game.id} +
{game.player_count}/4
+

+
+
+

Created by: {game.creator}

+

Created {time_ago(game.created_at)}

+
+
+ <.form :let={f} for={%{}} as={:join_game} phx-submit="join_game"> + <.input + type="hidden" + field={f[:game_code]} + id={"game_code_#{game.id}"} + value={game.id} + /> + + +
+
+
+ <% end %> +
+ <% end %>
+ + + <%= if @current_player && !Enum.empty?(@active_games) do %> +
+
+

Your Active Games

+
+ <%= for game <- @active_games do %> +
+
+
+

+ Game: {game.id} + <%= if game.your_turn do %> +
Your Turn!
+ <% end %> +

+
-
-

Lora Card Game - Serbian Variant - 4 players, 32-card deck

-
+
+

Players: {Enum.join(game.players, ", ")} ({game.player_count}/4)

+

+ <%= if game.playing do %> + Game in progress + <% else %> + Waiting for players + <% end %> + Last activity: {time_ago(game.last_activity)} +

+
+ +
+ <.link navigate={~p"/game/#{game.id}"} class="btn btn-sm btn-primary"> + Resume + +
+
+
+ <% end %> +
+
+
+ <% end %> + +
+ +
diff --git a/apps/lora_web/lib/lora_web/plugs/current_player.ex b/apps/lora_web/lib/lora_web/plugs/current_player.ex new file mode 100644 index 0000000..6c29119 --- /dev/null +++ b/apps/lora_web/lib/lora_web/plugs/current_player.ex @@ -0,0 +1,34 @@ +defmodule LoraWeb.Plugs.CurrentPlayer do + @moduledoc """ + Plug to assign the current player to the connection if authenticated. + + This plug doesn't enforce authentication but makes the player info available + if the user is authenticated. + """ + import Plug.Conn + + alias Lora.Accounts + + def init(opts), do: opts + + def call(conn, _opts) do + player_id = get_session(conn, "player_id") + + if player_id do + case Accounts.get_player(player_id) do + {:ok, player} -> + # User is authenticated, touch their session to prevent expiry + Accounts.touch_player(player_id) + assign(conn, :current_player, player) + + {:error, _} -> + # Player not found in ETS, remove from session + conn + |> delete_session("player_id") + |> assign(:current_player, nil) + end + else + assign(conn, :current_player, nil) + end + end +end diff --git a/apps/lora_web/lib/lora_web/plugs/require_auth.ex b/apps/lora_web/lib/lora_web/plugs/require_auth.ex new file mode 100644 index 0000000..58c10b8 --- /dev/null +++ b/apps/lora_web/lib/lora_web/plugs/require_auth.ex @@ -0,0 +1,45 @@ +defmodule LoraWeb.Plugs.RequireAuth do + @moduledoc """ + Plug to enforce authentication for certain routes. + + This plug ensures users are authenticated before accessing protected routes. + If not authenticated, it stores the intended path and redirects to the Auth0 login page. + """ + import Plug.Conn + import Phoenix.Controller + use LoraWeb, :verified_routes + + alias Lora.Accounts + + def init(opts), do: opts + + def call(conn, _opts) do + player_id = get_session(conn, "player_id") + + if player_id && player_exists?(player_id) do + # User is authenticated, touch their session to prevent expiry + Accounts.touch_player(player_id) + + # Add current_player to the connection assigns + {:ok, player} = Accounts.get_player(player_id) + assign(conn, :current_player, player) + else + # Save the current path for redirecting after login + target_path = conn.request_path + encoded_target_path = URI.encode_www_form(target_path) + + conn + |> put_session(:return_to, target_path) + |> redirect(to: ~p"/auth/auth0?state=#{encoded_target_path}") + |> halt() + end + end + + # Check if the player exists in the ETS store + defp player_exists?(player_id) do + case Accounts.get_player(player_id) do + {:ok, _player} -> true + {:error, _} -> false + end + end +end diff --git a/apps/lora_web/lib/lora_web/presence.ex b/apps/lora_web/lib/lora_web/presence.ex index 6a718dc..66e5b42 100644 --- a/apps/lora_web/lib/lora_web/presence.ex +++ b/apps/lora_web/lib/lora_web/presence.ex @@ -7,6 +7,6 @@ defmodule LoraWeb.Presence do Returns a topic string for tracking game-specific presence. """ def game_topic(game_id) when is_binary(game_id) do - "presence:game:#{game_id}" + "presence::game::#{game_id}" end end diff --git a/apps/lora_web/lib/lora_web/router.ex b/apps/lora_web/lib/lora_web/router.ex index f973719..166697d 100644 --- a/apps/lora_web/lib/lora_web/router.ex +++ b/apps/lora_web/lib/lora_web/router.ex @@ -8,21 +8,46 @@ defmodule LoraWeb.Router do plug :put_root_layout, html: {LoraWeb.Layouts, :root} plug :protect_from_forgery plug :put_secure_browser_headers - plug :ensure_player_id_generated + plug LoraWeb.Plugs.CurrentPlayer end pipeline :api do plug :accepts, ["json"] end + # Add authentication required pipeline + pipeline :auth_required do + plug LoraWeb.Plugs.RequireAuth + end + + # Unauthenticated routes scope "/", LoraWeb do pipe_through :browser # Replace the default route with our lobby - live "/", LobbyLive, :index + live_session :default, on_mount: {LoraWeb.LiveAuth, :default} do + live "/", LobbyLive, :index + end + end + + # Authentication routes + scope "/auth", LoraWeb do + pipe_through :browser + + # Auth0 routes handled by Ueberauth + get "/:provider", AuthController, :request + get "/:provider/callback", AuthController, :callback + delete "/logout", AuthController, :delete + end + + # Routes that require authentication + scope "/", LoraWeb do + pipe_through [:browser, :auth_required] # Game routes - live "/game/:id", GameLive, :show + live_session :authenticated, on_mount: {LoraWeb.LiveAuth, :default} do + live "/game/:id", GameLive, :show + end end # Other scopes may use custom stacks. @@ -46,19 +71,4 @@ defmodule LoraWeb.Router do forward "/mailbox", Plug.Swoosh.MailboxPreview end end - - # Ensure a player ID is generated and stored in the session - defp ensure_player_id_generated(conn, _opts) do - if get_session(conn, "player_id") do - conn - else - player_id = generate_player_id() - put_session(conn, "player_id", player_id) - end - end - - # Generate a random player ID - used in both production and tests - defp generate_player_id do - :crypto.strong_rand_bytes(16) |> Base.encode16(case: :lower) - end end diff --git a/apps/lora_web/priv/static/images/README.md b/apps/lora_web/priv/static/images/README.md new file mode 100644 index 0000000..ed18d37 --- /dev/null +++ b/apps/lora_web/priv/static/images/README.md @@ -0,0 +1 @@ +Playinhg Cards Taken from https://tekeye.uk/playing_cards/svg-playing-cards diff --git a/apps/lora_web/priv/static/images/backs/abstract.svg b/apps/lora_web/priv/static/images/backs/abstract.svg new file mode 100644 index 0000000..230ba70 --- /dev/null +++ b/apps/lora_web/priv/static/images/backs/abstract.svg @@ -0,0 +1,2692 @@ + + + + + Abstract + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Abstract + 2020-05-08 + + + Lazur URH + + + An abstract design. + + + + Lazur URH + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + abstract; blocks; + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/backs/abstract_clouds.svg b/apps/lora_web/priv/static/images/backs/abstract_clouds.svg new file mode 100644 index 0000000..424202e --- /dev/null +++ b/apps/lora_web/priv/static/images/backs/abstract_clouds.svg @@ -0,0 +1,2929 @@ + + + Abstract Clouds + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Abstract Clouds + + 2020-08-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + abstract; clouds; design; + + + An abstract clouds design. + + + Dordy + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/backs/abstract_scene.svg b/apps/lora_web/priv/static/images/backs/abstract_scene.svg new file mode 100644 index 0000000..99d6561 --- /dev/null +++ b/apps/lora_web/priv/static/images/backs/abstract_scene.svg @@ -0,0 +1,245 @@ + + + + + Abstract Scene + + + + + + + + + + image/svg+xml + + Abstract Scene + + 2020-05-08 + + + Daniel S. Fowler + + + + + dORDY + + + + + + + + + + Public Domain + + + en-GB + + + abstract; scene; landscape; colors; + + + An abstract scene. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/backs/astronaut.svg b/apps/lora_web/priv/static/images/backs/astronaut.svg new file mode 100644 index 0000000..c1bb10c --- /dev/null +++ b/apps/lora_web/priv/static/images/backs/astronaut.svg @@ -0,0 +1,3075 @@ + + + + + Struggling astronaut + + + + + + image/svg+xml + + Struggling astronaut + + 2020-05-08 + + + ric5sch + + + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + astronaut; space; struggle; + + + An astronaut struggling to reach his space craft. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/backs/blue.svg b/apps/lora_web/priv/static/images/backs/blue.svg new file mode 100644 index 0000000..0b9d1e3 --- /dev/null +++ b/apps/lora_web/priv/static/images/backs/blue.svg @@ -0,0 +1,821 @@ + +Blue Card Backimage/svg+xmlBlue Card Back2021-03-14Daniel S. FowlerPublic Domainhttps://tekeye.uken-GBblue; back; playing; card;https://tekeye.uk/playing_cards/svg-playing-cardsA playing card back pattern in blue. + + diff --git a/apps/lora_web/priv/static/images/backs/blue2.svg b/apps/lora_web/priv/static/images/backs/blue2.svg new file mode 100644 index 0000000..a49caea --- /dev/null +++ b/apps/lora_web/priv/static/images/backs/blue2.svg @@ -0,0 +1,813 @@ + +Another Blue Card Backimage/svg+xmlAnother Blue Card Back2020-03-13Daniel S. FowlerPublic Domainhttps://tekeye.uken-GBblue; back; playing; card;A blue patterned playing card back.https://tekeye.uk/playing_cards/svg-playing-cards + + diff --git a/apps/lora_web/priv/static/images/backs/cars.svg b/apps/lora_web/priv/static/images/backs/cars.svg new file mode 100644 index 0000000..e096e94 --- /dev/null +++ b/apps/lora_web/priv/static/images/backs/cars.svg @@ -0,0 +1,6177 @@ + + + Vintage red cars + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + https://tekeye.uk + + + Vintage red cars + 2020-05-08 + Vintage red cars + + + + Daniel S. Fowler + + + + + automobile; automotive; classic; cars; heritage; legacy; motor; racing; motoring; motorsports; transportation; vintage; red; green; + + + + + netalloy + + + + + Public Domain + + + en-GB + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/backs/castle.svg b/apps/lora_web/priv/static/images/backs/castle.svg new file mode 100644 index 0000000..6a17e95 --- /dev/null +++ b/apps/lora_web/priv/static/images/backs/castle.svg @@ -0,0 +1,3948 @@ + + + Castle Playing Card Back + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Castle Playing Card Back + + 2021-03-14 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + playing; card; rear; back; castle; moon; + + + A night time themed castle playing card back. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/backs/fish.svg b/apps/lora_web/priv/static/images/backs/fish.svg new file mode 100644 index 0000000..15f570f --- /dev/null +++ b/apps/lora_web/priv/static/images/backs/fish.svg @@ -0,0 +1,737 @@ + + + + + Copperband butterfly fish + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + https://tekeye.uk + + + Copperband butterfly fish + 2020-05-08 + A copperband butterfly fish. + + + + Daniel S. Fowler + + + + + beak; coralfish; chelmon; rostratus; copperband; butterfly; fish; reef; blue; + + + + + Public Domain + + + + + bravebug + + + en-GB + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/backs/frog.svg b/apps/lora_web/priv/static/images/backs/frog.svg new file mode 100644 index 0000000..e8825dc --- /dev/null +++ b/apps/lora_web/priv/static/images/backs/frog.svg @@ -0,0 +1,1947 @@ + + + Frog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + https://tekeye.uk + + + Frog + 2020-05-08 + Rainforest treefrog + + + + Daniel S. Fowler + + + + + amphibian; animal; frog; tree frog; treefrog + + + + + flooredmusic + + + en-GB + + + Public Domain + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/backs/png_96_dpi/abstract.png b/apps/lora_web/priv/static/images/backs/png_96_dpi/abstract.png new file mode 100644 index 0000000..47c9b3b Binary files /dev/null and b/apps/lora_web/priv/static/images/backs/png_96_dpi/abstract.png differ diff --git a/apps/lora_web/priv/static/images/backs/png_96_dpi/abstract_clouds.png b/apps/lora_web/priv/static/images/backs/png_96_dpi/abstract_clouds.png new file mode 100644 index 0000000..563556a Binary files /dev/null and b/apps/lora_web/priv/static/images/backs/png_96_dpi/abstract_clouds.png differ diff --git a/apps/lora_web/priv/static/images/backs/png_96_dpi/abstract_scene.png b/apps/lora_web/priv/static/images/backs/png_96_dpi/abstract_scene.png new file mode 100644 index 0000000..d85e83b Binary files /dev/null and b/apps/lora_web/priv/static/images/backs/png_96_dpi/abstract_scene.png differ diff --git a/apps/lora_web/priv/static/images/backs/png_96_dpi/astronaut.png b/apps/lora_web/priv/static/images/backs/png_96_dpi/astronaut.png new file mode 100644 index 0000000..87eeee6 Binary files /dev/null and b/apps/lora_web/priv/static/images/backs/png_96_dpi/astronaut.png differ diff --git a/apps/lora_web/priv/static/images/backs/png_96_dpi/blue.png b/apps/lora_web/priv/static/images/backs/png_96_dpi/blue.png new file mode 100644 index 0000000..f24b21a Binary files /dev/null and b/apps/lora_web/priv/static/images/backs/png_96_dpi/blue.png differ diff --git a/apps/lora_web/priv/static/images/backs/png_96_dpi/blue2.png b/apps/lora_web/priv/static/images/backs/png_96_dpi/blue2.png new file mode 100644 index 0000000..313e608 Binary files /dev/null and b/apps/lora_web/priv/static/images/backs/png_96_dpi/blue2.png differ diff --git a/apps/lora_web/priv/static/images/backs/png_96_dpi/cars.png b/apps/lora_web/priv/static/images/backs/png_96_dpi/cars.png new file mode 100644 index 0000000..0eab442 Binary files /dev/null and b/apps/lora_web/priv/static/images/backs/png_96_dpi/cars.png differ diff --git a/apps/lora_web/priv/static/images/backs/png_96_dpi/castle.png b/apps/lora_web/priv/static/images/backs/png_96_dpi/castle.png new file mode 100644 index 0000000..dad8d95 Binary files /dev/null and b/apps/lora_web/priv/static/images/backs/png_96_dpi/castle.png differ diff --git a/apps/lora_web/priv/static/images/backs/png_96_dpi/fish.png b/apps/lora_web/priv/static/images/backs/png_96_dpi/fish.png new file mode 100644 index 0000000..f10ad18 Binary files /dev/null and b/apps/lora_web/priv/static/images/backs/png_96_dpi/fish.png differ diff --git a/apps/lora_web/priv/static/images/backs/png_96_dpi/frog.png b/apps/lora_web/priv/static/images/backs/png_96_dpi/frog.png new file mode 100644 index 0000000..81f4f77 Binary files /dev/null and b/apps/lora_web/priv/static/images/backs/png_96_dpi/frog.png differ diff --git a/apps/lora_web/priv/static/images/backs/png_96_dpi/red.png b/apps/lora_web/priv/static/images/backs/png_96_dpi/red.png new file mode 100644 index 0000000..176afd7 Binary files /dev/null and b/apps/lora_web/priv/static/images/backs/png_96_dpi/red.png differ diff --git a/apps/lora_web/priv/static/images/backs/png_96_dpi/red2.png b/apps/lora_web/priv/static/images/backs/png_96_dpi/red2.png new file mode 100644 index 0000000..5160634 Binary files /dev/null and b/apps/lora_web/priv/static/images/backs/png_96_dpi/red2.png differ diff --git a/apps/lora_web/priv/static/images/backs/red.svg b/apps/lora_web/priv/static/images/backs/red.svg new file mode 100644 index 0000000..0f6cf1b --- /dev/null +++ b/apps/lora_web/priv/static/images/backs/red.svg @@ -0,0 +1,804 @@ + +Red Card Backimage/svg+xmlRed Card Back2021-03-14Daniel S. FowlerPublic Domainhttps://tekeye.uken-GBblue; back; playing; card;Playing card back, red patterned design.https://tekeye.uk/playing_cards/svg-playing-cards + + + diff --git a/apps/lora_web/priv/static/images/backs/red2.svg b/apps/lora_web/priv/static/images/backs/red2.svg new file mode 100644 index 0000000..a5283f0 --- /dev/null +++ b/apps/lora_web/priv/static/images/backs/red2.svg @@ -0,0 +1,808 @@ + +Red Card Backimage/svg+xmlRed Card Back2020-05-08Daniel S. FowlerPublic Domainhttps://tekeye.uken-GBred; back; playing; card;A red pattered playing card back. + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_10.svg b/apps/lora_web/priv/static/images/fronts/clubs_10.svg new file mode 100644 index 0000000..df60b05 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_10.svg @@ -0,0 +1,434 @@ + + + + + Ten of Clubs Playing Card + + + + + + + + image/svg+xml + + Ten of Clubs Playing Card + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + ten; clubs; playing; card; black; white; + + + A ten of clubs playing card in black and white. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_2.svg b/apps/lora_web/priv/static/images/fronts/clubs_2.svg new file mode 100644 index 0000000..b94a59c --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_2.svg @@ -0,0 +1,225 @@ + + + + + Two of Clubs Playing Card + + + + + + + + image/svg+xml + + Two of Clubs Playing Card + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + playing; card; two; clubs; black; white; + + + A two of clubs playing card + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_3.svg b/apps/lora_web/priv/static/images/fronts/clubs_3.svg new file mode 100644 index 0000000..10cb11a --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_3.svg @@ -0,0 +1,246 @@ + + + + + Three of Clubs Playing Card + + + + + + + + image/svg+xml + + Three of Clubs Playing Card + 2020-05-04 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + three; clubs; playing; card; black; white; + + + A three of clubs playing card in black and white. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_4.svg b/apps/lora_web/priv/static/images/fronts/clubs_4.svg new file mode 100644 index 0000000..e7c21d4 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_4.svg @@ -0,0 +1,269 @@ + + + Four of Clubs Playing Card + + + + + + + + image/svg+xml + + Four of Clubs Playing Card + + 2020-05-04 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + four; clubs; playing; card; white; black; + + + A four of clubs playing card in black and white. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_5.svg b/apps/lora_web/priv/static/images/fronts/clubs_5.svg new file mode 100644 index 0000000..cb7dc18 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_5.svg @@ -0,0 +1,284 @@ + + + Five of Clubs Playing Card + + + + + + + + image/svg+xml + + Five of Clubs Playing Card + + 2020-05-04 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + five; clubs; playing; card; white; black + + + A five of clubs playing card + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_6.svg b/apps/lora_web/priv/static/images/fronts/clubs_6.svg new file mode 100644 index 0000000..f9909f6 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_6.svg @@ -0,0 +1,303 @@ + + + + + Six of Clubs Playing Card + + + + + + + + image/svg+xml + + Six of Clubs Playing Card + + 2020-05-04 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + six; clubs; playing; card; white; + + + A six of clubs playing card in black and white. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_7.svg b/apps/lora_web/priv/static/images/fronts/clubs_7.svg new file mode 100644 index 0000000..506040c --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_7.svg @@ -0,0 +1,321 @@ + + + + + Seven of Clubs Playing Card + + + + + + + + image/svg+xml + + Seven of Clubs Playing Card + + 2020-05-04 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + seven; clubs; playing; card; white; black; + + + A seven of clubs playing card, in black and white. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_8.svg b/apps/lora_web/priv/static/images/fronts/clubs_8.svg new file mode 100644 index 0000000..dc15581 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_8.svg @@ -0,0 +1,342 @@ + + + + + EIght of Clubs Playing Card + + + + + + + + image/svg+xml + + EIght of Clubs Playing Card + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + eight; clubs; playing; card; black; white; + + + An eight of clubs playing card in black and white. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_9.svg b/apps/lora_web/priv/static/images/fronts/clubs_9.svg new file mode 100644 index 0000000..22b49eb --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_9.svg @@ -0,0 +1,357 @@ + + + + + Nine of Clubs Playing Card + + + + + + + + image/svg+xml + + Nine of Clubs Playing Card + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + nine; clubs; playing; card; blak; white; + + + A nine of clubs playing card in black and white. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_ace.svg b/apps/lora_web/priv/static/images/fronts/clubs_ace.svg new file mode 100644 index 0000000..00b376d --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_ace.svg @@ -0,0 +1,231 @@ + + + + + Ace of Clubs Playing Card + + + + + + + + image/svg+xml + + Ace of Clubs Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + ace; clubs; playing; card; + + + An ace of clubs playing card in black and white. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_jack.svg b/apps/lora_web/priv/static/images/fronts/clubs_jack.svg new file mode 100644 index 0000000..6a66c7d --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_jack.svg @@ -0,0 +1,261 @@ + + + + + Jack of Clubs Playing Card + + + + + + + + image/svg+xml + + Jack of Clubs Playing Card + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + jack; clubs; playing; card; + + + A jack of clubs playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_king.svg b/apps/lora_web/priv/static/images/fronts/clubs_king.svg new file mode 100644 index 0000000..d63cb1f --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_king.svg @@ -0,0 +1,263 @@ + + + + + King of Clubs Playing Card + + + + + + + + image/svg+xml + + King of Clubs Playing Card + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + king; clubs; playing; card; + + + A king of clubs playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/clubs_queen.svg b/apps/lora_web/priv/static/images/fronts/clubs_queen.svg new file mode 100644 index 0000000..a9e71c6 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/clubs_queen.svg @@ -0,0 +1,265 @@ + + + + + Queen of Clubs Playing Card + + + + + + + + image/svg+xml + + Queen of Clubs Playing Card + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + queen; clubs; playing; card; + + + A queen of clubs playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_10.svg b/apps/lora_web/priv/static/images/fronts/diamonds_10.svg new file mode 100644 index 0000000..b6b8e1f --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_10.svg @@ -0,0 +1,269 @@ + + + + + Ten of Diamonds Playing Card + + + + + + + + image/svg+xml + + Ten of Diamonds Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + Ten; diamonds; playing; card; white; red; + + + A ten of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_2.svg b/apps/lora_web/priv/static/images/fronts/diamonds_2.svg new file mode 100644 index 0000000..42b08c7 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_2.svg @@ -0,0 +1,185 @@ + + + + + Two of Diamonds Playing Card + + + + + + + + image/svg+xml + + Two of Diamonds Playing Card + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + two; diamonds; playing; card; white; red; + + + A two of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_3.svg b/apps/lora_web/priv/static/images/fronts/diamonds_3.svg new file mode 100644 index 0000000..935ae2e --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_3.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_4.svg b/apps/lora_web/priv/static/images/fronts/diamonds_4.svg new file mode 100644 index 0000000..e13f0ea --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_4.svg @@ -0,0 +1,190 @@ + + + + + Four of Diamonds Playing Card + + + + + + + + image/svg+xml + + Four of Diamonds Playing Card + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + Four; diamonds; playing; card; white; red; + + + A four of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_5.svg b/apps/lora_web/priv/static/images/fronts/diamonds_5.svg new file mode 100644 index 0000000..4cf496c --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_5.svg @@ -0,0 +1,209 @@ + + + + + Five of Diamonds Playing Card + + + + + + + + image/svg+xml + + Five of Diamonds Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + Five; diamonds; playing; card; white; red; + + + A five of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_6.svg b/apps/lora_web/priv/static/images/fronts/diamonds_6.svg new file mode 100644 index 0000000..45fdd16 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_6.svg @@ -0,0 +1,217 @@ + + + + + Six of Diamonds Playing Card + + + + + + + + image/svg+xml + + Six of Diamonds Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + six; diamonds; playing; card; white; red; + + + A six of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_7.svg b/apps/lora_web/priv/static/images/fronts/diamonds_7.svg new file mode 100644 index 0000000..3fd1592 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_7.svg @@ -0,0 +1,227 @@ + + + + + Seven of Diamonds Playing Card + + + + + + + + image/svg+xml + + Seven of Diamonds Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + seven; diamonds; playing; card; white; red; + + + A seven of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_8.svg b/apps/lora_web/priv/static/images/fronts/diamonds_8.svg new file mode 100644 index 0000000..198c9ce --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_8.svg @@ -0,0 +1,233 @@ + + + + + Eight of Diamonds Playing Card + + + + + + + + image/svg+xml + + Eight of Diamonds Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + eight; diamonds; playing; card; white; red; + + + A eight of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_9.svg b/apps/lora_web/priv/static/images/fronts/diamonds_9.svg new file mode 100644 index 0000000..ffd6743 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_9.svg @@ -0,0 +1,243 @@ + + + + + Nine of Diamonds Playing Card + + + + + + + + image/svg+xml + + Nine of Diamonds Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + nine; diamonds; playing; card; white; red; + + + A nine of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_ace.svg b/apps/lora_web/priv/static/images/fronts/diamonds_ace.svg new file mode 100644 index 0000000..10a5ac0 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_ace.svg @@ -0,0 +1,177 @@ + + + + + Ace of Diamonds Playing Card + + + + + + + + image/svg+xml + + Ace of Diamonds Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + ace; diamonds; playing; card; white; red; + + + An ace of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_jack.svg b/apps/lora_web/priv/static/images/fronts/diamonds_jack.svg new file mode 100644 index 0000000..d053cf2 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_jack.svg @@ -0,0 +1,217 @@ + + + + + Jack of Diamonds Playing Card + + + + + + + + image/svg+xml + + Jack of Diamonds Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + jack; diamonds; playing; card; + + + A jack of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_king.svg b/apps/lora_web/priv/static/images/fronts/diamonds_king.svg new file mode 100644 index 0000000..f6c03a8 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_king.svg @@ -0,0 +1,226 @@ + + + + + King of Diamonds Playing Card + + + + + + + + image/svg+xml + + King of Diamonds Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + king; diamonds; playing; card; white; red; + + + A king of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/diamonds_queen.svg b/apps/lora_web/priv/static/images/fronts/diamonds_queen.svg new file mode 100644 index 0000000..0288fcf --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/diamonds_queen.svg @@ -0,0 +1,225 @@ + + + + + Queen of Diamonds Playing Card + + + + + + + + image/svg+xml + + Queen of Diamonds Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + queen; diamonds; playing; card; white; red; + + + A queen of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_10.svg b/apps/lora_web/priv/static/images/fronts/hearts_10.svg new file mode 100644 index 0000000..b9b75f1 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_10.svg @@ -0,0 +1,255 @@ + + + + + Ten of Hearts Playing Card + + + + + + + + image/svg+xml + + Ten of Hearts Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + ten; hearts; playing; card; white; red; + + + A ten of hearts playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_2.svg b/apps/lora_web/priv/static/images/fronts/hearts_2.svg new file mode 100644 index 0000000..dece7d4 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_2.svg @@ -0,0 +1,183 @@ + + + + + Two of Diamonds Playing Card + + + + + + + + image/svg+xml + + Two of Diamonds Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + two; diamonds; playing; card; white; red; + + + A two of diamonds playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_3.svg b/apps/lora_web/priv/static/images/fronts/hearts_3.svg new file mode 100644 index 0000000..354a96f --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_3.svg @@ -0,0 +1,178 @@ + + + + + Three of Hearts Playing Card + + + + + + + + image/svg+xml + + Three of Hearts Playing Card + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + three; hearts; playing; card; white; red; + + + A three of hearts playing card. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_4.svg b/apps/lora_web/priv/static/images/fronts/hearts_4.svg new file mode 100644 index 0000000..bae0873 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_4.svg @@ -0,0 +1,199 @@ + + + + + Four of Hearts Playing Card + + + + + + + + image/svg+xml + + Four of Hearts Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + four; hearts; playing; card; white; red; + + + A four of hearts playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_5.svg b/apps/lora_web/priv/static/images/fronts/hearts_5.svg new file mode 100644 index 0000000..ecb040c --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_5.svg @@ -0,0 +1,205 @@ + + + + + Five of Hearts Playing Card + + + + + + + + image/svg+xml + + Five of Hearts Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + five; hearts; playing; card; white; red; + + + A five of hearts playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_6.svg b/apps/lora_web/priv/static/images/fronts/hearts_6.svg new file mode 100644 index 0000000..5b68e8b --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_6.svg @@ -0,0 +1,208 @@ + + + + + Six of Hearts Playing Card + + + + + + + + image/svg+xml + + Six of Hearts Playing Card + + 2024-11-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + six; hearts; playing; card; white; red; + + + A six of hearts playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_7.svg b/apps/lora_web/priv/static/images/fronts/hearts_7.svg new file mode 100644 index 0000000..e44961d --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_7.svg @@ -0,0 +1,217 @@ + + + + + Seven of Hearts Playing Card + + + + + + + + image/svg+xml + + Seven of Hearts Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + seven; hearts; playing; card; white; red; + + + A seven of hearts playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_8.svg b/apps/lora_web/priv/static/images/fronts/hearts_8.svg new file mode 100644 index 0000000..c74063d --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_8.svg @@ -0,0 +1,223 @@ + + + + + Eight of Hearts Playing Card + + + + + + + + image/svg+xml + + Eight of Hearts Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + eight; hearts; playing; card; white; red; + + + A eight of hearts playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_9.svg b/apps/lora_web/priv/static/images/fronts/hearts_9.svg new file mode 100644 index 0000000..a7f62d9 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_9.svg @@ -0,0 +1,229 @@ + + + + + Nine of Hearts Playing Card + + + + + + + + image/svg+xml + + Nine of Hearts Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + nine; hearts; playing; card; white; red; + + + A nine of hearts playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_ace.svg b/apps/lora_web/priv/static/images/fronts/hearts_ace.svg new file mode 100644 index 0000000..c797c1d --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_ace.svg @@ -0,0 +1,177 @@ + + + + + Ace of Hearts Playing Card + + + + + + + + image/svg+xml + + Ace of Hearts Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + ace; hearts; playing; card; white; red; + + + An ace of hearts playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_jack.svg b/apps/lora_web/priv/static/images/fronts/hearts_jack.svg new file mode 100644 index 0000000..e582e65 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_jack.svg @@ -0,0 +1,228 @@ + + + + + Jack of Hearts Playing Card + + + + + + + + image/svg+xml + + Jack of Hearts Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + jack; hearts; playing; card; + + + A jack of hearts playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_king.svg b/apps/lora_web/priv/static/images/fronts/hearts_king.svg new file mode 100644 index 0000000..bb4af6e --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_king.svg @@ -0,0 +1,215 @@ + + + + + King of Hearts Playing Card + + + + + + + + image/svg+xml + + King of Hearts Playing Card + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + king; hearts; playing; card; + + + A king of hearts playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/hearts_queen.svg b/apps/lora_web/priv/static/images/fronts/hearts_queen.svg new file mode 100644 index 0000000..1e1973c --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/hearts_queen.svg @@ -0,0 +1,234 @@ + + + + + Queen of Hearts Playing Card + + + + + + + + image/svg+xml + + Queen of Hearts Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + queen; hearts; playing; card; + + + A queen of hearts playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/joker_black.svg b/apps/lora_web/priv/static/images/fronts/joker_black.svg new file mode 100644 index 0000000..ce500dd --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/joker_black.svg @@ -0,0 +1,1034 @@ + + + Black Joker Playing Card + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Black Joker Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + black; joker; playing; card; + + + A black joker playing card. + + + + + + + + + + + + + + + J + O + K + E + R + J + O + K + E + R + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/joker_red.svg b/apps/lora_web/priv/static/images/fronts/joker_red.svg new file mode 100644 index 0000000..408d516 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/joker_red.svg @@ -0,0 +1,898 @@ + + + Red Joker Playing Card + + + + + + + + + + + + + + + image/svg+xml + + Red Joker Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + red; joker; playing; card; + + + A red joker playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_10.svg b/apps/lora_web/priv/static/images/fronts/spades_10.svg new file mode 100644 index 0000000..dabed47 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_10.svg @@ -0,0 +1,251 @@ + + + + + Ten of Spades Playing Card + + + + + + + + image/svg+xml + + Ten of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + ten; spades; playing; card; black; white + + + A ten of spades playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_2.svg b/apps/lora_web/priv/static/images/fronts/spades_2.svg new file mode 100644 index 0000000..caaabfc --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_2.svg @@ -0,0 +1,183 @@ + + + + + Two of Spades Playing Card + + + + + + + + image/svg+xml + + Two of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + two; spades; playing; card; black; white; + + + A two of spades playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_3.svg b/apps/lora_web/priv/static/images/fronts/spades_3.svg new file mode 100644 index 0000000..34c30c1 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_3.svg @@ -0,0 +1,189 @@ + + + + + Three of Spades Playing Card + + + + + + + + image/svg+xml + + Three of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + three; spades; playing; card; black; white + + + A three of spades playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_4.svg b/apps/lora_web/priv/static/images/fronts/spades_4.svg new file mode 100644 index 0000000..84cb34e --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_4.svg @@ -0,0 +1,195 @@ + + + + + Four of Spades Playing Card + + + + + + + + image/svg+xml + + Four of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + four; spades; playing; card; black; white + + + A four of spades playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_5.svg b/apps/lora_web/priv/static/images/fronts/spades_5.svg new file mode 100644 index 0000000..f675611 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_5.svg @@ -0,0 +1,201 @@ + + + + + Five of Spades Playing Card + + + + + + + + image/svg+xml + + Five of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + five; spades; playing; card; black; white + + + A five of spades playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_6.svg b/apps/lora_web/priv/static/images/fronts/spades_6.svg new file mode 100644 index 0000000..9d74b62 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_6.svg @@ -0,0 +1,207 @@ + + + + + Six of Spades Playing Card + + + + + + + + image/svg+xml + + Six of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + six; spades; playing; card; black; white + + + A six of spades playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_7.svg b/apps/lora_web/priv/static/images/fronts/spades_7.svg new file mode 100644 index 0000000..83b5bc8 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_7.svg @@ -0,0 +1,213 @@ + + + + + Seven of Spades Playing Card + + + + + + + + image/svg+xml + + Seven of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + seven; spades; playing; card; black; white + + + A seven of spades playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_8.svg b/apps/lora_web/priv/static/images/fronts/spades_8.svg new file mode 100644 index 0000000..3c285c8 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_8.svg @@ -0,0 +1,219 @@ + + + + + Eight of Spades Playing Card + + + + + + + + image/svg+xml + + Eight of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + eight; spades; playing; card; black; white + + + An eight of spades playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_9.svg b/apps/lora_web/priv/static/images/fronts/spades_9.svg new file mode 100644 index 0000000..5f7fa86 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_9.svg @@ -0,0 +1,202 @@ + + + + + Nine of Spades Playing Card + + + + + + + + image/svg+xml + + Nine of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + nine; spades; playing; card; black; white + + + A nine of spades playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_ace.svg b/apps/lora_web/priv/static/images/fronts/spades_ace.svg new file mode 100644 index 0000000..0ba09de --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_ace.svg @@ -0,0 +1,483 @@ + + + + + Ace of Spades Playing Card + + + + + + + + image/svg+xml + + Ace of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + ace; spades; playing; card; black; white + + + An ace of spades playing card in black and white with a phrase. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_jack.svg b/apps/lora_web/priv/static/images/fronts/spades_jack.svg new file mode 100644 index 0000000..6e9abff --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_jack.svg @@ -0,0 +1,223 @@ + + + + + Jack of Spades Playing Card + + + + + + + + image/svg+xml + + Jack of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + jack; spades; playing; card; + + + A jack of spades playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_king.svg b/apps/lora_web/priv/static/images/fronts/spades_king.svg new file mode 100644 index 0000000..ed92539 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_king.svg @@ -0,0 +1,214 @@ + + + + + King of Spades Playing Card + + + + + + + + image/svg+xml + + King of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + king; spades; playing; card; + + + A king of spades playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/priv/static/images/fronts/spades_queen.svg b/apps/lora_web/priv/static/images/fronts/spades_queen.svg new file mode 100644 index 0000000..cf22370 --- /dev/null +++ b/apps/lora_web/priv/static/images/fronts/spades_queen.svg @@ -0,0 +1,226 @@ + + + + + Queen of Spades Playing Card + + + + + + + + image/svg+xml + + Queen of Spades Playing Card + + 2020-05-05 + + + Daniel S. Fowler + + + + + Public Domain + + + + + https://tekeye.uk + + + en-GB + + + queen; spades; playing; card; + + + A queen of spades playing card. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/lora_web/test/lora_web/auth_test.exs b/apps/lora_web/test/lora_web/auth_test.exs new file mode 100644 index 0000000..8cf9d5c --- /dev/null +++ b/apps/lora_web/test/lora_web/auth_test.exs @@ -0,0 +1,139 @@ +defmodule LoraWeb.AuthTest do + use LoraWeb.ConnCase + + import Mock + + alias Lora.Accounts + alias Lora.Accounts.Player + + # Mock Auth0 response + @auth0_response %Ueberauth.Auth{ + provider: :auth0, + strategy: Ueberauth.Strategy.Helpers, + uid: "auth0|12345678", + info: %{ + name: "Test User", + email: "test@example.com", + nickname: "testuser" + }, + credentials: %{ + token: "abc123", + expires: true, + expires_at: 1_622_222_222, + refresh_token: "def456" + }, + extra: %{} + } + + # Setup Accounts ETS table for tests + setup do + # Check if the ETS table already exists + case :ets.whereis(:players) do + :undefined -> Accounts.init() + # Table already exists + _ -> :ok + end + + :ok + end + + describe "auth flow" do + test "successful authentication redirects to the requested route", %{conn: conn} do + # Set up a mock response for Ueberauth + conn = + conn + |> Plug.Test.init_test_session(%{}) + |> assign(:ueberauth_auth, @auth0_response) + |> get("/auth/auth0/callback", %{"state" => "/lobby#create"}) + + # Check that we have a session with player_id and a redirect to the game URL + assert redirected_to(conn) =~ "/game/" + assert get_session(conn, "player_id") == "auth0|12345678" + + # Verify player was stored in ETS + {:ok, player} = Accounts.get_player("auth0|12345678") + assert player.name == "Test User" + assert player.email == "test@example.com" + assert player.sub == "auth0|12345678" + end + + test "failed authentication redirects to lobby with error", %{conn: conn} do + # Simulate failed Auth0 callback + conn = + conn + |> Plug.Test.init_test_session(%{}) + |> assign(:ueberauth_failure, %{errors: [%{message: "Invalid credentials"}]}) + |> get("/auth/auth0/callback") + + # Check for redirect to lobby and error flash + assert redirected_to(conn) == "/" + assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Authentication failed" + end + + test "logout removes the player from ETS and session", %{conn: conn} do + # Setup a player in the system + player = %Player{ + id: "test-id", + sub: "test-id", + name: "Test User", + email: "test@example.com", + inserted_at: DateTime.utc_now() + } + + {:ok, _} = Accounts.store_player(player) + + # Logout with a session containing the player_id + conn = + conn + |> Plug.Test.init_test_session(%{"player_id" => "test-id"}) + |> delete("/auth/logout") + + # Verify the session is cleared and we're redirected to lobby + assert redirected_to(conn) == "/" + refute get_session(conn, "player_id") + + # Verify the player was removed from ETS + assert {:error, _} = Accounts.get_player("test-id") + end + end + + describe "authentication guards" do + test "RequireAuth redirects unauthenticated users to login", %{conn: conn} do + conn = + conn + |> get("/game/123456") + + # The RequireAuth plug should redirect to Auth0 login + assert redirected_to(conn) =~ "/auth/auth0" + assert redirected_to(conn) =~ "%252Fgame%252F123456" + end + + test "authenticated users can access protected routes", %{conn: conn} do + # Setup a player in the system + player = %Player{ + id: "test-id", + sub: "test-id", + name: "Test User", + email: "test@example.com", + inserted_at: DateTime.utc_now() + } + + {:ok, _} = Accounts.store_player(player) + + # Mock the game exists function to allow the request + with_mock Lora, + get_game_state: fn _game_id -> {:ok, %{players: [], phase: :lobby}} end, + player_reconnect: fn _game_id, _player_id, _pid -> :ok end, + add_player: fn _game_id, _player_id, _player_name -> {:ok, %{players: []}} end, + game_exists?: fn _game_id -> true end do + conn = + conn + |> Plug.Test.init_test_session(%{"player_id" => "test-id"}) + |> get("/game/123456") + + # Should not redirect + assert html_response(conn, 200) + end + end + end +end diff --git a/apps/lora_web/test/lora_web/live/game_live_test.exs b/apps/lora_web/test/lora_web/live/game_live_test.exs index 4a04687..21c1977 100644 --- a/apps/lora_web/test/lora_web/live/game_live_test.exs +++ b/apps/lora_web/test/lora_web/live/game_live_test.exs @@ -1,8 +1,9 @@ defmodule LoraWeb.GameLiveTest do use LoraWeb.LiveViewCase import Mock + alias Lora.Accounts.Player - test "displays warning when player info is missing", %{conn: _conn} do + test "displays game when authenticated", %{conn: conn} do game_id = "TESTID" # Mock the Lora.get_game_state function with a complete game state structure @@ -21,6 +22,17 @@ defmodule LoraWeb.GameLiveTest do dealt_count: 0 } + # Setup a player in the system + player = %Player{ + id: "test-id", + sub: "test-id", + name: "Test User", + email: "test@example.com", + inserted_at: DateTime.utc_now() + } + + {:ok, _} = Lora.Accounts.store_player(player) + # Mock Lora module functions with_mocks([ {Lora, [], @@ -28,11 +40,14 @@ defmodule LoraWeb.GameLiveTest do get_game_state: fn _id -> {:ok, mock_game_state} end, add_player: fn _game_id, _player_id, _player_name -> {:ok, mock_game_state} end, player_reconnect: fn _game_id, _player_id, _pid -> :ok end, - legal_moves: fn _game_id, _player_id -> {:ok, []} end + legal_moves: fn _game_id, _player_id -> {:ok, []} end, + game_exists?: fn _id -> true end ]} ]) do - # This is a clean conn with no session data - conn = Phoenix.ConnTest.build_conn() + # Initialize session with authenticated player + conn = + conn + |> Plug.Test.init_test_session(%{"player_id" => "test-id"}) # Connect to the game {:ok, _view, html} = live(conn, "/game/#{game_id}") @@ -40,7 +55,7 @@ defmodule LoraWeb.GameLiveTest do # Verify game shows waiting state assert html =~ "Waiting for game to start" # Verify that we've successfully loaded the game with dealer info - assert html =~ "Dealer: Empty Seat 1" + assert html =~ "Empty Seat 1" # Verify contract info is displayed assert html =~ "Minimum" assert html =~ "Plus one point per trick taken" diff --git a/apps/lora_web/test/lora_web/live/lobby_live_test.exs b/apps/lora_web/test/lora_web/live/lobby_live_test.exs index 2a6d29b..7ffe54d 100644 --- a/apps/lora_web/test/lora_web/live/lobby_live_test.exs +++ b/apps/lora_web/test/lora_web/live/lobby_live_test.exs @@ -3,48 +3,39 @@ defmodule LoraWeb.LobbyLiveTest do test "renders lobby form", %{conn: conn} do {:ok, _view, html} = live(conn, "/") - assert html =~ "Create Game" - assert html =~ "Join Game" + assert html =~ "Create a New Game" + assert html =~ "Join an Existing Game" end test "create game - shows error with empty player name", %{conn: conn} do - {:ok, view, _html} = live(conn, "/") - - # Submit form with empty player name - html = - view - |> element("#create-game-form") - |> render_submit(%{create_player: %{name: ""}}) + # When not signed in, there should be a warning about signing in + {:ok, _view, html} = live(conn, "/") - # Verify error message is shown - assert html =~ "Please enter a valid name" + # Verify message is shown + assert html =~ "Please sign in to create a game" end - test "join game - shows error with empty player name", %{conn: conn} do - game_id = "TESTID" - + test "join game - shows error with empty game code", %{conn: conn} do {:ok, view, _html} = live(conn, "/") - # Submit form with empty player name + # Submit form with empty game code html = view |> element("#join-game-form") - |> render_submit(%{join_player: %{name: "", game_code: game_id}}) + |> render_submit(%{join_player: %{game_code: ""}}) - # Verify error message is shown - assert html =~ "Please enter a valid name" + # Should show an error + assert html =~ "Please enter a valid game code" end - test "join game - shows error with empty game ID", %{conn: conn} do - player_name = "TestPlayer" - + test "join game - shows error with invalid game code", %{conn: conn} do {:ok, view, _html} = live(conn, "/") - # Submit form with empty game ID + # Submit form with invalid game code html = view |> element("#join-game-form") - |> render_submit(%{join_player: %{name: player_name, game_code: ""}}) + |> render_submit(%{join_player: %{game_code: "ABC"}}) # Verify error message is shown assert html =~ "Please enter a valid game code" diff --git a/config/config.exs b/config/config.exs index 9ab2344..7df278b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -32,6 +32,22 @@ config :lora_web, LoraWeb.Endpoint, pubsub_server: Lora.PubSub, live_view: [signing_salt: "ss8bVTun"] +# Configure Ueberauth +config :ueberauth, Ueberauth, + providers: [ + auth0: {Ueberauth.Strategy.Auth0, []} + ] + +# Configure Ueberauth Auth0 provider +config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, + domain: System.get_env("AUTH0_DOMAIN", "tri-mudraca-dev.eu.auth0.com"), + client_id: System.get_env("AUTH0_CLIENT_ID", "ty88vQoOuTjekRCQPalFDsnK0eL0gyog"), + client_secret: + System.get_env( + "AUTH0_CLIENT_SECRET", + "8_I9bfVRxjbFekL8ZQkXGq6w23TdcT0juSBedoPeVtJaYYheO6wwy8pZ1L1sURfR" + ) + # Configure esbuild (the version is required) config :esbuild, version: "0.17.11", diff --git a/config/dev.exs b/config/dev.exs index 0802a20..1bd611c 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,5 +1,8 @@ import Config +# Set debug mode for development +config :lora_web, :debug_mode, true + # For development, we disable any cache and enable # debugging and code reloading. # diff --git a/config/prod.exs b/config/prod.exs index 54e1f37..53b7323 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1,5 +1,8 @@ import Config +# Set debug mode for production +config :lora_web, :debug_mode, false + # Note we also include the path to a cache manifest # containing the digested version of static files. This # manifest is generated by the `mix phx.digest` task, diff --git a/config/runtime.exs b/config/runtime.exs index fa707f4..cb9f8da 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -7,20 +7,20 @@ import Config # any compile-time configuration in here, as it won't be applied. # The block below contains prod specific runtime configuration. if config_env() == :prod do - database_url = - System.get_env("DATABASE_URL") || - raise """ - environment variable DATABASE_URL is missing. - For example: ecto://USER:PASS@HOST/DATABASE - """ + # database_url = + # System.get_env("DATABASE_URL") || + # raise """ + # environment variable DATABASE_URL is missing. + # For example: ecto://USER:PASS@HOST/DATABASE + # """ - maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] + # maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] - config :lora, Lora.Repo, - # ssl: true, - url: database_url, - pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), - socket_options: maybe_ipv6 + # config :lora, Lora.Repo, + # # ssl: true, + # url: database_url, + # pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), + # socket_options: maybe_ipv6 import Config @@ -43,14 +43,17 @@ if config_env() == :prod do ip: {0, 0, 0, 0, 0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000") ], - secret_key_base: secret_key_base + url: [host: System.get_env("PHX_HOST") || "localhost", port: System.get_env("PORT") || 4000], + check_origin: false, + secret_key_base: secret_key_base, + server: true # ## Using releases # # If you are doing OTP releases, you need to instruct Phoenix # to start each relevant endpoint: # - # config :lora_web, LoraWeb.Endpoint, server: true + # config :lora_web, LoraWeb.Endpoint, server: true # # Then you can assemble a release by calling `mix release`. # See `mix help release` for more information. diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..1970013 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,3 @@ +services: + lora: + image: ${APP_IMAGE:-gcr.io/exponentially/lora:latest} diff --git a/mix.exs b/mix.exs index 7e17518..45f1bca 100644 --- a/mix.exs +++ b/mix.exs @@ -12,9 +12,17 @@ defmodule Lora.Umbrella.MixProject do preferred_cli_env: [ "test.with_coverage": :test ], + releases: [ + lora: [ + applications: [ + lora: :permanent, + lora_web: :permanent + ] + ] + ], test_coverage: [ tool: ExCoveralls, - summary: [threshold: 68], + summary: [threshold: 40], ignore_modules: [ # Skip web modules for now - they're not part of the current unit testing phase Lora.DataCase, diff --git a/mix.lock b/mix.lock index 02af0f2..2a433cd 100644 --- a/mix.lock +++ b/mix.lock @@ -25,6 +25,7 @@ "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"}, "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"}, "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, @@ -42,8 +43,11 @@ "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"}, + "tesla": {:hex, :tesla, "1.14.1", "71c5b031b4e089c0fbfb2b362e24b4478465773ae4ef569760a8c2899ad1e73c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c1dde8140a49a3bef5bb622356e77ac5a24ad0c8091f12c3b7fc1077ce797155"}, "thousand_island": {:hex, :thousand_island, "1.3.13", "d598c609172275f7b1648c9f6eddf900e42312b09bfc2f2020358f926ee00d39", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5a34bdf24ae2f965ddf7ba1a416f3111cfe7df50de8d66f6310e01fc2e80b02a"}, "tidewave": {:hex, :tidewave, "0.1.6", "f07514ee2c348c2e682a2632309ac6d8ec425392bfb803955a6bb19ca5508e2f", [:mix], [{:circular_buffer, "~> 0.4", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.47 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "3708592f325e1f54b99b215cd8c38f726732451bf5cfa16d73584793f99d9da4"}, + "ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"}, + "ueberauth_auth0": {:hex, :ueberauth_auth0, "2.1.0", "0632d5844049fa2f26823f15e1120aa32f27df6f27ce515a4b04641736594bf4", [:mix], [{:oauth2, "~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "8d3b30fa27c95c9e82c30c4afb016251405706d2e9627e603c3c9787fd1314fc"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, }