Compare commits

..

No commits in common. "2696e1f184ffcbaacfaa52d4f5468aee584e8b4f" and "83bc98115225524a492a092582a2f0a03bc8bad8" have entirely different histories.

175 changed files with 21332 additions and 1803 deletions

19
.eslintrc.js Normal file
View file

@ -0,0 +1,19 @@
/**
* @type {import('@types/eslint').Linter.BaseConfig}
*/
module.exports = {
extends: [
"@remix-run/eslint-config",
"@remix-run/eslint-config/node",
"@remix-run/eslint-config/jest",
"prettier",
],
// we're using vitest which has a very similar API to jest
// (so the linting plugins work nicely), but it we have to explicitly
// set the jest version.
settings: {
jest: {
version: 27,
},
},
};

7
.gitattributes vendored
View file

@ -1,7 +0,0 @@
# See https://git-scm.com/docs/gitattributes for more about git attribute files.
# Mark the database schema as having been generated.
db/schema.rb linguist-generated
# Mark any vendored files as having been vendored.
vendor/* linguist-vendored

43
.gitignore vendored
View file

@ -1,38 +1,11 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
node_modules
# Ignore bundler config.
/.bundle
/build
/public/build
.env
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-*
/cypress/screenshots
/cypress/videos
/postgres-data
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore pidfiles, but keep the directory.
/tmp/pids/*
!/tmp/pids/
!/tmp/pids/.keep
# Ignore uploaded files in development.
/storage/*
!/storage/.keep
/tmp/storage/*
!/tmp/storage/
!/tmp/storage/.keep
/public/assets
# Ignore master key for decrypting credentials and more.
/config/master.key
postgres-data/
.env
/app/styles/tailwind.css

View file

@ -1 +0,0 @@
system

53
Dockerfile Normal file
View file

@ -0,0 +1,53 @@
# base node image
FROM node:16-bullseye-slim as base
# set for base and all layer that inherit from it
ENV NODE_ENV production
# Install openssl for Prisma
RUN apt-get update && apt-get install -y openssl
# Install all node_modules, including dev dependencies
FROM base as deps
WORKDIR /myapp
ADD package.json package-lock.json ./
RUN npm install --production=false
# Setup production node_modules
FROM base as production-deps
WORKDIR /myapp
COPY --from=deps /myapp/node_modules /myapp/node_modules
ADD package.json package-lock.json ./
RUN npm prune --production
# Build the app
FROM base as build
WORKDIR /myapp
COPY --from=deps /myapp/node_modules /myapp/node_modules
ADD prisma .
RUN npx prisma generate
ADD . .
RUN npm run postinstall
RUN npm run build
# Finally, build the production image with minimal footprint
FROM base
WORKDIR /myapp
COPY --from=production-deps /myapp/node_modules /myapp/node_modules
COPY --from=build /myapp/node_modules/.prisma /myapp/node_modules/.prisma
COPY --from=build /myapp/build /myapp/build
COPY --from=build /myapp/public /myapp/public
ADD . .
CMD ["npm", "start"]

80
Gemfile
View file

@ -1,80 +0,0 @@
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.1.1"
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.0.2", ">= 7.0.2.3"
# gem "devise"
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "sprockets-rails"
# Use sqlite3 as the database for Active Record
gem "sqlite3", "~> 1.4"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", "~> 5.0"
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"
gem "omniauth"
gem "omniauth-discord"
# gem "omniauth-rails_csrf_protection"
gem "dotenv-rails"
# Use Redis adapter to run Action Cable in production
# gem "redis", "~> 4.0"
# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false
# Use Sass to process CSS
# gem "sassc-rails"
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri mingw x64_mingw ]
end
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
gem "web-console"
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
# gem "rack-mini-profiler"
# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
# gem "spring"
end
group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara"
gem "selenium-webdriver"
gem "webdrivers"
end

View file

@ -1,268 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.2.3)
actionpack (= 7.0.2.3)
activesupport (= 7.0.2.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.2.3)
actionpack (= 7.0.2.3)
activejob (= 7.0.2.3)
activerecord (= 7.0.2.3)
activestorage (= 7.0.2.3)
activesupport (= 7.0.2.3)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.2.3)
actionpack (= 7.0.2.3)
actionview (= 7.0.2.3)
activejob (= 7.0.2.3)
activesupport (= 7.0.2.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.2.3)
actionview (= 7.0.2.3)
activesupport (= 7.0.2.3)
rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.2.3)
actionpack (= 7.0.2.3)
activerecord (= 7.0.2.3)
activestorage (= 7.0.2.3)
activesupport (= 7.0.2.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.2.3)
activesupport (= 7.0.2.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.2.3)
activesupport (= 7.0.2.3)
globalid (>= 0.3.6)
activemodel (7.0.2.3)
activesupport (= 7.0.2.3)
activerecord (7.0.2.3)
activemodel (= 7.0.2.3)
activesupport (= 7.0.2.3)
activestorage (7.0.2.3)
actionpack (= 7.0.2.3)
activejob (= 7.0.2.3)
activerecord (= 7.0.2.3)
activesupport (= 7.0.2.3)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.2.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
bindex (0.8.1)
bootsnap (1.11.1)
msgpack (~> 1.2)
builder (3.2.4)
capybara (3.36.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
childprocess (4.1.0)
concurrent-ruby (1.1.10)
crass (1.0.6)
debug (1.5.0)
irb (>= 1.3.6)
reline (>= 0.2.7)
digest (3.1.0)
dotenv (2.7.6)
dotenv-rails (2.7.6)
dotenv (= 2.7.6)
railties (>= 3.2)
erubi (1.10.0)
faraday (2.2.0)
faraday-net_http (~> 2.0)
ruby2_keywords (>= 0.0.4)
faraday-net_http (2.0.1)
globalid (1.0.0)
activesupport (>= 5.0)
hashie (5.0.0)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
importmap-rails (1.0.3)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.5.11)
irb (1.4.1)
reline (>= 0.3.0)
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jwt (2.3.0)
loofah (2.16.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (1.0.2)
matrix (0.4.2)
method_source (1.0.0)
mini_mime (1.1.2)
minitest (5.15.0)
msgpack (1.5.1)
multi_json (1.15.0)
multi_xml (0.6.0)
net-imap (0.2.3)
digest
net-protocol
strscan
net-pop (0.1.1)
digest
net-protocol
timeout
net-protocol (0.1.3)
timeout
net-smtp (0.3.1)
digest
net-protocol
timeout
nio4r (2.5.8)
nokogiri (1.13.3-x86_64-linux)
racc (~> 1.4)
oauth2 (1.4.9)
faraday (>= 0.17.3, < 3.0)
jwt (>= 1.0, < 3.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
omniauth (2.0.4)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
rack-protection
omniauth-discord (1.0.2)
omniauth (~> 2.0.4)
omniauth-oauth2
omniauth-oauth2 (1.7.2)
oauth2 (~> 1.4)
omniauth (>= 1.9, < 3)
public_suffix (4.0.6)
puma (5.6.4)
nio4r (~> 2.0)
racc (1.6.0)
rack (2.2.3)
rack-protection (2.2.0)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (7.0.2.3)
actioncable (= 7.0.2.3)
actionmailbox (= 7.0.2.3)
actionmailer (= 7.0.2.3)
actionpack (= 7.0.2.3)
actiontext (= 7.0.2.3)
actionview (= 7.0.2.3)
activejob (= 7.0.2.3)
activemodel (= 7.0.2.3)
activerecord (= 7.0.2.3)
activestorage (= 7.0.2.3)
activesupport (= 7.0.2.3)
bundler (>= 1.15.0)
railties (= 7.0.2.3)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.2)
loofah (~> 2.3)
railties (7.0.2.3)
actionpack (= 7.0.2.3)
activesupport (= 7.0.2.3)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rake (13.0.6)
regexp_parser (2.3.0)
reline (0.3.1)
io-console (~> 0.5)
rexml (3.2.5)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
selenium-webdriver (4.1.0)
childprocess (>= 0.5, < 5.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2)
sprockets (4.0.3)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sqlite3 (1.4.2)
stimulus-rails (1.0.4)
railties (>= 6.0.0)
strscan (3.0.1)
thor (1.2.1)
timeout (0.2.0)
turbo-rails (1.0.1)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
web-console (4.2.0)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webdrivers (5.0.0)
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (~> 4.0)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.5.4)
PLATFORMS
x86_64-linux
DEPENDENCIES
bootsnap
capybara
debug
dotenv-rails
importmap-rails
jbuilder
omniauth
omniauth-discord
puma (~> 5.0)
rails (~> 7.0.2, >= 7.0.2.3)
selenium-webdriver
sprockets-rails
sqlite3 (~> 1.4)
stimulus-rails
turbo-rails
tzinfo-data
web-console
webdrivers
RUBY VERSION
ruby 3.1.1p18
BUNDLED WITH
2.3.7

199
README.md
View file

@ -1,24 +1,197 @@
# README
# Remix Blues Stack
This README would normally document whatever steps are necessary to get the
application up and running.
![The Remix Blues Stack](https://repository-images.githubusercontent.com/461012689/37d5bd8b-fa9c-4ab0-893c-f0a199d5012d)
Things you may want to cover:
Learn more about [Remix Stacks](https://remix.run/stacks).
* Ruby version
```
npx create-remix --template remix-run/blues-stack
```
* System dependencies
## What's in the stack
* Configuration
- [Multi-region Fly app deployment](https://fly.io/docs/reference/scaling/) with [Docker](https://www.docker.com/)
- [Multi-region Fly PostgreSQL Cluster](https://fly.io/docs/getting-started/multi-region-databases/)
- Healthcheck endpoint for [Fly backups region fallbacks](https://fly.io/docs/reference/configuration/#services-http_checks)
- [GitHub Actions](https://github.com/features/actions) for deploy on merge to production and staging environments
- Email/Password Authentication with [cookie-based sessions](https://remix.run/docs/en/v1/api/remix#createcookiesessionstorage)
- Database ORM with [Prisma](https://prisma.io)
- Styling with [Tailwind](https://tailwindcss.com/)
- End-to-end testing with [Cypress](https://cypress.io)
- Local third party request mocking with [MSW](https://mswjs.io)
- Unit testing with [Vitest](https://vitest.dev) and [Testing Library](https://testing-library.com)
- Code formatting with [Prettier](https://prettier.io)
- Linting with [ESLint](https://eslint.org)
- Static Types with [TypeScript](https://typescriptlang.org)
* Database creation
Not a fan of bits of the stack? Fork it, change it, and use `npx create-remix --template your/repo`! Make it your own.
* Database initialization
## Development
* How to run the test suite
- Start the Postgres Database in [Docker](https://www.docker.com/get-started):
* Services (job queues, cache servers, search engines, etc.)
```sh
npm run docker
```
* Deployment instructions
> **Note:** The npm script will complete while Docker sets up the container in the background. Ensure that Docker has finished and your container is running before proceeding.
* ...
- Initial setup:
```sh
npm run setup
```
- Start dev server:
```sh
npm run dev
```
> **Note:** You may see a nasty error in the PM2 logs when you initially run the dev script. This should only appear once and will not affect your local app server. We are working on improving this!
This starts your app in development mode, rebuilding assets on file changes.
The database seed script creates a new user with some data you can use to get started:
- Email: `rachel@remix.run`
- Password: `racheliscool`
If you'd prefer not to use Docker, you can also use Fly's Wireguard VPN to connect to a development database (or even your production database). You can find the instructions to set up Wireguard [here](https://fly.io/docs/reference/private-networking/#install-your-wireguard-app), and the instructions for creating a development database [here](https://fly.io/docs/reference/postgres/).
### Relevant code:
This is a pretty simple note-taking app, but it's a good example of how you can build a full stack app with Prisma and Remix. The main functionality is creating users, logging in and out, and creating and deleting notes.
- creating users, and logging in and out [./app/models/user.server.ts](./app/models/user.server.ts)
- user sessions, and verifying them [./app/session.server.ts](./app/session.server.ts)
- creating, and deleting notes [./app/models/note.server.ts](./app/models/note.server.ts)
## Deployment
This Remix Stack comes with two GitHub Actions that handle automatically deploying your app to production and staging environments.
Prior to your first deployment, you'll need to do a few things:
- [Install Fly](https://fly.io/docs/getting-started/installing-flyctl/)
- Sign up and log in to Fly
```sh
fly auth signup
```
> **Note:** If you have more than one Fly account, ensure that you are signed into the same account in the Fly CLI as you are in the browser. In your terminal, run `fly auth whoami` and ensure the email matches the Fly account signed into the browser.
- Create two apps on Fly, one for staging and one for production:
```sh
fly create border-server-a1df
fly create border-server-a1df-staging
```
- Initialize Git.
```sh
git init
```
- Create a new [GitHub Repository](https://repo.new), and then add it as the remote for your project. **Do not push your app yet!**
```sh
git remote add origin <ORIGIN_URL>
```
- Add a `FLY_API_TOKEN` to your GitHub repo. To do this, go to your user settings on Fly and create a new [token](https://web.fly.io/user/personal_access_tokens/new), then add it to [your repo secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) with the name `FLY_API_TOKEN`.
- Add a `SESSION_SECRET` to your fly app secrets, to do this you can run the following commands:
```sh
fly secrets set SESSION_SECRET=$(openssl rand -hex 32) --app border-server-a1df
fly secrets set SESSION_SECRET=$(openssl rand -hex 32) --app border-server-a1df-staging
```
> **Note:** When creating the staging secret, you may get a warning from the Fly CLI that looks like this:
>
> ```
> WARN app flag 'border-server-a1df-staging' does not match app name in config file 'border-server-a1df'
> ```
>
> This simply means that the current directory contains a config that references the production app we created in the first step. Ignore this warning and proceed to create the secret.
If you don't have openssl installed, you can also use [1password](https://1password.com/generate-password) to generate a random secret, just replace `$(openssl rand -hex 32)` with the generated secret.
- Create a database for both your staging and production environments. Run the following:
```sh
fly postgres create --name border-server-a1df-db
fly postgres attach --postgres-app border-server-a1df-db --app border-server-a1df
fly postgres create --name border-server-a1df-staging-db
fly postgres attach --postgres-app border-server-a1df-staging-db --app border-server-a1df-staging
```
> **Note:** You'll get the same warning for the same reason when attaching the staging database that you did in the `fly set secret` step above. No worries. Proceed!
Fly will take care of setting the `DATABASE_URL` secret for you.
Now that every is set up you can commit and push your changes to your repo. Every commit to your `main` branch will trigger a deployment to your production environment, and every commit to your `dev` branch will trigger a deployment to your staging environment.
### Multi-region deploys
Once you have your site and database running in a single region, you can add more regions by following [Fly's Scaling](https://fly.io/docs/reference/scaling/) and [Multi-region PostgreSQL](https://fly.io/docs/getting-started/multi-region-databases/) docs.
Make certain to set a `PRIMARY_REGION` environment variable for your app. You can use `[env]` config in the `fly.toml` to set that to the region you want to use as the primary region for both your app and database.
#### Testing your app in other regions
Install the [ModHeader](https://modheader.com/) browser extension (or something similar) and use it to load your app with the header `fly-prefer-region` set to the region name you would like to test.
You can check the `x-fly-region` header on the response to know which region your request was handled by.
## GitHub Actions
We use GitHub Actions for continuous integration and deployment. Anything that gets into the `main` branch will be deployed to production after running tests/build/etc. Anything in the `dev` branch will be deployed to staging.
## Testing
### Cypress
We use Cypress for our End-to-End tests in this project. You'll find those in the `cypress` directory. As you make changes, add to an existing file or create a new file in the `cypress/e2e` directory to test your changes.
We use [`@testing-library/cypress`](https://testing-library.com/cypress) for selecting elements on the page semantically.
To run these tests in development, run `npm run test:e2e:dev` which will start the dev server for the app as well as the Cypress client. Make sure the database is running in docker as described above.
We have a utility for testing authenticated features without having to go through the login flow:
```ts
cy.login();
// you are now logged in as a new user
```
We also have a utility to auto-delete the user at the end of your test. Just make sure to add this in each test file:
```ts
afterEach(() => {
cy.cleanupUser();
});
```
That way, we can keep your local db clean and keep your tests isolated from one another.
### Vitest
For lower level tests of utilities and individual components, we use `vitest`. We have DOM-specific assertion helpers via [`@testing-library/jest-dom`](https://testing-library.com/jest-dom).
### Type Checking
This project uses TypeScript. It's recommended to get TypeScript set up for your editor to get a really great in-editor experience with type checking and auto-complete. To run type checking across the whole project, run `npm run typecheck`.
### Linting
This project uses ESLint for linting. That is configured in `.eslintrc.js`.
### Formatting
We use [Prettier](https://prettier.io/) for auto-formatting in this project. It's recommended to install an editor plugin (like the [VSCode Prettier plugin](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)) to get auto-formatting on save. There's also a `npm run format` script you can run to format all files in the project.

View file

@ -1,6 +0,0 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative "config/application"
Rails.application.load_tasks

View file

@ -1,4 +0,0 @@
//= link_tree ../images
//= link_directory ../stylesheets .css
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js

View file

@ -1,15 +0,0 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/

View file

@ -1,4 +0,0 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View file

@ -1,4 +0,0 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end

View file

@ -1,2 +0,0 @@
class ApplicationController < ActionController::Base
end

View file

@ -1,2 +0,0 @@
class DiscordUsersController < ApplicationController
end

View file

@ -1,2 +0,0 @@
class SessionsController < ApplicationController
end

View file

@ -1,70 +0,0 @@
class UsersController < ApplicationController
before_action :set_user, only: %i[ show edit update destroy ]
# GET /users or /users.json
def index
@users = User.all
end
# GET /users/1 or /users/1.json
def show
end
# GET /users/new
def new
@user = User.new
end
# GET /users/1/edit
def edit
end
# POST /users or /users.json
def create
@user = User.new(user_params)
respond_to do |format|
if @user.save
format.html { redirect_to user_url(@user), notice: "User was successfully created." }
format.json { render :show, status: :created, location: @user }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /users/1 or /users/1.json
def update
respond_to do |format|
if @user.update(user_params)
format.html { redirect_to user_url(@user), notice: "User was successfully updated." }
format.json { render :show, status: :ok, location: @user }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
# DELETE /users/1 or /users/1.json
def destroy
@user.destroy
respond_to do |format|
format.html { redirect_to users_url, notice: "User was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
@user = User.find(params[:id])
end
# Only allow a list of trusted parameters through.
def user_params
params.require(:user).permit(:provider, :uid, :border)
end
end

62
app/db.server.ts Normal file
View file

@ -0,0 +1,62 @@
import { PrismaClient } from "@prisma/client";
import invariant from "tiny-invariant";
let prisma: PrismaClient;
declare global {
var __db__: PrismaClient;
}
// this is needed because in development we don't want to restart
// the server with every change, but we want to make sure we don't
// create a new connection to the DB with every change either.
// in production we'll have a single connection to the DB.
if (process.env.NODE_ENV === "production") {
prisma = getClient();
} else {
if (!global.__db__) {
global.__db__ = getClient();
}
prisma = global.__db__;
}
function getClient() {
const { DATABASE_URL } = process.env;
invariant(typeof DATABASE_URL === "string", "DATABASE_URL env var not set");
const databaseUrl = new URL(DATABASE_URL);
const isLocalHost = databaseUrl.hostname === "localhost";
const PRIMARY_REGION = isLocalHost ? null : process.env.PRIMARY_REGION;
const FLY_REGION = isLocalHost ? null : process.env.FLY_REGION;
const isReadReplicaRegion = !PRIMARY_REGION || PRIMARY_REGION === FLY_REGION;
if (!isLocalHost) {
databaseUrl.host = `${FLY_REGION}.${databaseUrl.host}`;
if (!isReadReplicaRegion) {
// 5433 is the read-replica port
databaseUrl.port = "5433";
}
}
console.log(`🔌 setting up prisma client to ${databaseUrl.host}`);
// NOTE: during development if you change anything in this function, remember
// that this only runs once per server restart and won't automatically be
// re-run per request like everything else is. So if you need to change
// something in this file, you'll need to manually restart the server.
const client = new PrismaClient({
datasources: {
db: {
url: databaseUrl.toString(),
},
},
});
// connect eagerly
client.$connect();
return client;
}
export { prisma };

124
app/discord/index.ts Normal file
View file

@ -0,0 +1,124 @@
import axios from 'axios';
import { prisma } from "~/db.server";
import { createUserSession } from '~/session.server';
export interface DiscordUser {
id: string;
username: string;
discriminator: string;
avatar: string;
bot?: boolean;
system?: boolean;
mfa_enabled?: boolean;
}
export interface AccessTokenResponse {
access_token: string;
expires: number;
refresh_token: string;
}
export function getAccessToken(code: string): Promise<AccessTokenResponse> {
return new Promise(
(resolve, reject) => {
const body = new FormData();
body.append("grant_type", "authorization_code");
body.append("code", code);
body.append("redirect_url", process.env.DISCORD_REDIRECT_URI || "");
axios.post("https://discord.com/api/oauth2/token", body)
.then((res) => {
const { access_token, refresh_token, expires_in } = res.data;
resolve({
access_token,
refresh_token,
expires: Date.now() + expires_in * 1000
});
})
.catch(reject);
}
)
}
export async function authorize(access_code: string): Promise<AccessTokenResponse | undefined> {
return new Promise(
(resolve, reject) => {
const formData = new FormData();
formData.append("client_id", process.env.DISCORD_CLIENT_ID || "");
formData.append("client_secret", process.env.DISCORD_CLIENT_SECRET || "");
formData.append("grant_type", "authorization_code");
formData.append("redirect_uri", process.env.DISCORD_REDIRECT_URI || "");
formData.append("scope", "identify");
formData.append("code", access_code);
axios.post("https://discord.com/api/oauth2/token", formData)
.then((res) => {
const { access_token, refresh_token, expires_in } = res.data;
resolve({
access_token,
refresh_token,
expires: Date.now() + expires_in * 1000
});
})
.catch(reject);
}
)
}
export function getDiscordUser(token: string): Promise<DiscordUser> {
return new Promise(
(resolve, reject) => {
axios.get("https://discord.com/api/users/@me", {
headers: {
Authorization: `Bearer ${token}`
}
})
.then(
(res) => {
resolve(res.data);
}
)
.catch(reject);
}
)
}
export async function discordLogin(request: Request, code: string): Promise<Response> {
return new Promise(
(resolve, reject) => {
getAccessToken(code)
.then((token) => {
getDiscordUser(token.access_token)
.then(async (user) => {
prisma.discordUser.upsert({
create: {
discord_id: user.id,
access_token: token.access_token,
refresh_token: token.refresh_token,
expires: new Date(token.expires),
username: user.username,
discriminator: user.discriminator,
},
update: {
discord_id: user.id,
access_token: token.access_token,
refresh_token: token.refresh_token,
expires: new Date(token.expires),
username: user.username,
discriminator: user.discriminator,
},
where: {
discord_id: user.id
}
})
.then(async () => {
resolve(await createUserSession({request, discord_id: user.id, redirectTo: '/'}));
})
})
})
}
)
}

4
app/entry.client.tsx Normal file
View file

@ -0,0 +1,4 @@
import { hydrate } from "react-dom";
import { RemixBrowser } from "remix";
hydrate(<RemixBrowser />, document);

21
app/entry.server.tsx Normal file
View file

@ -0,0 +1,21 @@
import { renderToString } from "react-dom/server";
import { RemixServer } from "remix";
import type { EntryContext } from "remix";
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
const markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);
responseHeaders.set("Content-Type", "text/html");
return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders,
});
}

View file

@ -1,2 +0,0 @@
module ApplicationHelper
end

View file

@ -1,2 +0,0 @@
module DiscordUsersHelper
end

View file

@ -1,2 +0,0 @@
module SessionsHelper
end

View file

@ -1,2 +0,0 @@
module UsersHelper
end

View file

@ -1,3 +0,0 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"

View file

@ -1,9 +0,0 @@
import { Application } from "@hotwired/stimulus"
const application = Application.start()
// Configure Stimulus development experience
application.debug = false
window.Stimulus = application
export { application }

View file

@ -1,7 +0,0 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.element.textContent = "Hello World!"
}
}

View file

@ -1,11 +0,0 @@
// Import and register all your controllers from the importmap under controllers/*
import { application } from "controllers/application"
// Eager load all controllers defined in the import map under controllers/**/*_controller
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)
// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
// lazyLoadControllersFrom("controllers", application)

View file

@ -1,7 +0,0 @@
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked
# Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError
end

View file

@ -1,4 +0,0 @@
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout "mailer"
end

View file

@ -1,3 +0,0 @@
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
end

View file

@ -1,4 +0,0 @@
class DiscordUser < ApplicationRecord
attr_accessible :discord_id, :username, :discriminator, :avatar
validates :discord_id, :uniqueness => true
end

View file

@ -1,2 +0,0 @@
class User < ApplicationRecord
end

65
app/models/user.server.ts Normal file
View file

@ -0,0 +1,65 @@
import { prisma } from "~/db.server";
import type {User} from "@prisma/client";
import { AccessTokenResponse, DiscordUser } from "~/discord/index";
export type {User, Border} from "@prisma/client";
export async function getUserByDiscordId(discord_id: User["discord_id"]) {
return prisma.user.findUnique({ where: { discord_id: discord_id || undefined }});
}
export async function getUserById(id: User["id"]) {
return prisma.user.findUnique({ where: { id } });
}
/// SHOULD ONLY BE USED WITH A CORRESPONDING OAUTH TOKEN
export async function createUser(discord_id: User["discord_id"]) {
return prisma.user.create({
data: {
discord_id
},
});
}
export async function createDiscordLogin(discord_id: DiscordUser["id"], token_response: AccessTokenResponse) {
return prisma.discordUser.create({
data: {
discord_id,
access_token: token_response.access_token,
refresh_token: token_response.refresh_token,
expires: new Date(token_response.expires)
}
})
}
export type SessionInformation = {
bearer_token: string,
refresh_token: string
};
export async function discordIdentify(bearer_token: string, refresh_token: string): Promise<DiscordUser | SessionInformation | undefined> {
let user_info = await (
await fetch("https://discord.com/api/users/@me", {
headers: {
Authorization: `Bearer ${bearer_token}`
}
})
).json();
if (!user_info["id"]) {
const form_data = new FormData();
form_data.append("client_id", process.env.DISCORD_CLIENT_ID || "");
form_data.append("client_secret", process.env.DISCORD_CLIENT_SECRET || "");
form_data.append("grant_type", "refresh_token");
form_data.append("refresh_token", refresh_token);
let refresh_info = await (await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
body: form_data
})).json();
return refresh_info;
}
return user_info;
}

40
app/root.tsx Normal file
View file

@ -0,0 +1,40 @@
import {
json,
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from "remix";
import type { LinksFunction, MetaFunction, LoaderFunction } from "remix";
import tailwindStylesheetUrl from "./styles/tailwind.css";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
};
export const meta: MetaFunction = () => ({
charset: "utf-8",
title: "Border Selector",
viewport: "width=device-width,initial-scale=1",
});
export default function App() {
return (
<html lang="en" className="h-full">
<head>
<Meta />
<Links />
</head>
<body className="h-full">
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}

View file

@ -0,0 +1,23 @@
// learn more: https://fly.io/docs/reference/configuration/#services-http_checks
import type { LoaderFunction } from "remix";
import { prisma } from "~/db.server";
export const loader: LoaderFunction = async ({ request }) => {
const host =
request.headers.get("X-Forwarded-Host") ?? request.headers.get("host");
try {
// if we can connect to the database and make a simple query
// and make a HEAD request to ourselves, then we're good.
await Promise.all([
prisma.user.count(),
fetch(`http://${host}`, { method: "HEAD" }).then((r) => {
if (!r.ok) return Promise.reject(r);
}),
]);
return new Response("OK");
} catch (error: unknown) {
console.log("healthcheck ❌", { error });
return new Response("ERROR", { status: 500 });
}
};

23
app/routes/index.tsx Normal file
View file

@ -0,0 +1,23 @@
import { json, Link, LoaderFunction, useLoaderData } from "remix";
import { getUser, getSession } from "~/session.server";
import { useOptionalUser } from "~/utils";
export const loader: LoaderFunction = async ({ request }) => {
const user_info = await getUser(request);
return json(user_info);
}
export default function Index() {
const discordUser = useLoaderData();
console.log("discordUser is", discordUser);
return (
<main className="relative min-h-screen bg-white sm:flex sm:items-center sm:justify-center">
<h1>Do you love the color of the sky?</h1>
<br />
<Link
to="/loginstart">
Log in
</Link>
</main>
);
}

39
app/routes/login.tsx Normal file
View file

@ -0,0 +1,39 @@
import * as React from "react";
import type { ActionFunction, LoaderFunction, MetaFunction } from "remix";
import {
Form,
json,
Link,
useActionData,
redirect,
useSearchParams,
} from "remix";
import { discordLogin } from "~/discord";
import { createUserSession, getUserId } from "~/session.server";
export const loader: LoaderFunction = async ({ request }) => {
const userId = await getUserId(request);
if (userId) return redirect("/");
const url = new URL(await request.url);
const accessCode = url.searchParams.get("code") || "";
const response = await discordLogin(request, accessCode);
console.log(response);
return ""
};
export const meta: MetaFunction = () => {
return {
title: "Login",
};
};
export default function LoginPage() {
return (
<div className="flex min-h-full flex-col justify-center">
<p>Error logging in.</p>
</div>
)
}

25
app/routes/loginstart.tsx Normal file
View file

@ -0,0 +1,25 @@
import type { LoaderFunction, MetaFunction } from "remix";
import {
json,
redirect,
} from "remix";
import { getUserId } from "~/session.server";
export const loader: LoaderFunction = async ({ request }) => {
const userId = await getUserId(request);
if (userId) return redirect("/");
const client_id = process.env.DISCORD_CLIENT_ID || "";
const redirect_uri = process.env.DISCORD_REDIRECT_URI || "";
return redirect(`https://discord.com/api/oauth2/authorize?client_id=${client_id}` +
`&response_type=code&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=identify`);
};
export const meta: MetaFunction = () => {
return {
title: "Login",
};
};
export default function LoginPage() {
return <div></div>
}

11
app/routes/logout.tsx Normal file
View file

@ -0,0 +1,11 @@
import type { ActionFunction, LoaderFunction } from "remix";
import { redirect } from "remix";
import { logout } from "~/session.server";
export const action: ActionFunction = async ({ request }) => {
return logout(request);
};
export const loader: LoaderFunction = async () => {
return redirect("/");
};

97
app/session.server.ts Normal file
View file

@ -0,0 +1,97 @@
import { createCookieSessionStorage, redirect } from "remix";
import invariant from "tiny-invariant";
import { discordIdentify, User, SessionInformation } from "~/models/user.server";
import { getUserByDiscordId } from "~/models/user.server";
invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set");
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
maxAge: 0,
path: "/",
sameSite: "lax",
secrets: [process.env.SESSION_SECRET],
secure: process.env.NODE_ENV === "production",
},
});
const USER_SESSION_KEY = "userId";
const BEARER_TOKEN_KEY = "bearerToken";
const REFRESH_TOKEN_KEY = "refreshToken";
export async function getSession(request: Request) {
const cookie = request.headers.get("Cookie");
return sessionStorage.getSession(cookie);
}
export async function getUserId(request: Request): Promise<string | undefined> {
const session = await getSession(request);
const userId = session.get(USER_SESSION_KEY);
return userId;
}
export async function getUser(request: Request): Promise<null | User> {
const userId = await getUserId(request);
if (userId === undefined) return null;
const user = await getUserByDiscordId(userId);
if (user) return user;
throw await logout(request);
}
export async function requireUserId(
request: Request,
redirectTo: string = new URL(request.url).pathname
): Promise<string> {
const userId = await getUserId(request);
if (!userId) {
const searchParams = new URLSearchParams([["redirectTo", redirectTo]]);
throw redirect(`/loginstart?${searchParams}`);
}
return userId;
}
export async function requireUser(request: Request) {
const session = await getSession(request);
const user_id = session.get(USER_SESSION_KEY);
const client_id = process.env.DISCORD_CLIENT_ID || "";
const redirect_uri = process.env.DISCORD_REDIRECT_URI || "";
if (!user_id) {
return redirect(`https://discord.com/api/oauth2/authorize?client_id=${client_id}` +
`&response_type=code&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=identify`);
}
}
export async function createUserSession({
request,
discord_id,
redirectTo,
}: {
request: Request;
discord_id: string;
redirectTo: string;
}) {
const session = await getSession(request);
session.set(USER_SESSION_KEY, discord_id);
console.log(discord_id);
return redirect(redirectTo, {
headers: {
"Set-Cookie": await sessionStorage.commitSession(session, {
maxAge: 60 * 60 * 24 * 7 // 7 days
}),
},
});
}
export async function logout(request: Request) {
const session = await getSession(request);
return redirect("/", {
headers: {
"Set-Cookie": await sessionStorage.destroySession(session),
},
});
}

44
app/utils.ts Normal file
View file

@ -0,0 +1,44 @@
import { useMemo } from "react";
import { useMatches } from "remix";
import type { User } from "~/models/user.server";
/**
* This base hook is used in other hooks to quickly search for specific data
* across all loader data using useMatches.
* @param {string} id The route id
* @returns {JSON|undefined} The router data or undefined if not found
*/
export function useMatchesData(
id: string
): Record<string, unknown> | undefined {
const matchingRoutes = useMatches();
const route = useMemo(
() => matchingRoutes.find((route) => route.id === id),
[matchingRoutes, id]
);
return route?.data;
}
function isUser(user: any): user is User {
return user && typeof user === "object" && typeof user.discord_id === "string";
}
export function useOptionalUser(): User | undefined {
const data = useMatchesData("root");
console.log(data)
if (!data || !isUser(data.user)) {
return undefined;
}
return data.user;
}
export function useUser(): User {
const maybeUser = useOptionalUser();
if (!maybeUser) {
throw new Error(
"No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead."
);
}
return maybeUser;
}

View file

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Borders</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<%= yield %>
</body>
</html>

View file

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Email styles need to be inline */
</style>
</head>
<body>
<%= yield %>
</body>
</html>

View file

@ -1 +0,0 @@
<%= yield %>

View file

@ -1,32 +0,0 @@
<%= form_with(model: user) do |form| %>
<% if user.errors.any? %>
<div style="color: red">
<h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% user.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :provider, style: "display: block" %>
<%= form.text_field :provider %>
</div>
<div>
<%= form.label :uid, style: "display: block" %>
<%= form.text_field :uid %>
</div>
<div>
<%= form.label :border, style: "display: block" %>
<%= form.text_field :border %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>

View file

@ -1,17 +0,0 @@
<div id="<%= dom_id user %>">
<p>
<strong>Provider:</strong>
<%= user.provider %>
</p>
<p>
<strong>Uid:</strong>
<%= user.uid %>
</p>
<p>
<strong>Border:</strong>
<%= user.border %>
</p>
</div>

View file

@ -1,2 +0,0 @@
json.extract! user, :id, :provider, :uid, :border, :created_at, :updated_at
json.url user_url(user, format: :json)

View file

@ -1,10 +0,0 @@
<h1>Editing user</h1>
<%= render "form", user: @user %>
<br>
<div>
<%= link_to "Show this user", @user %> |
<%= link_to "Back to users", users_path %>
</div>

View file

@ -1,14 +0,0 @@
<p style="color: green"><%= notice %></p>
<h1>Users</h1>
<div id="users">
<% @users.each do |user| %>
<%= render user %>
<p>
<%= link_to "Show this user", user %>
</p>
<% end %>
</div>
<%= link_to "New user", new_user_path %>

View file

@ -1 +0,0 @@
json.array! @users, partial: "users/user", as: :user

View file

@ -1,9 +0,0 @@
<h1>New user</h1>
<%= render "form", user: @user %>
<br>
<div>
<%= link_to "Back to users", users_path %>
</div>

View file

@ -1,10 +0,0 @@
<p style="color: green"><%= notice %></p>
<%= render @user %>
<div>
<%= link_to "Edit this user", edit_user_path(@user) %> |
<%= link_to "Back to users", users_path %>
<%= button_to "Destroy this user", @user, method: :delete %>
</div>

View file

@ -1 +0,0 @@
json.partial! "users/user", user: @user

View file

@ -1,114 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'bundle' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "rubygems"
m = Module.new do
module_function
def invoked_as_script?
File.expand_path($0) == File.expand_path(__FILE__)
end
def env_var_version
ENV["BUNDLER_VERSION"]
end
def cli_arg_version
return unless invoked_as_script? # don't want to hijack other binstubs
return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
bundler_version = nil
update_index = nil
ARGV.each_with_index do |a, i|
if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
bundler_version = a
end
next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
bundler_version = $1
update_index = i
end
bundler_version
end
def gemfile
gemfile = ENV["BUNDLE_GEMFILE"]
return gemfile if gemfile && !gemfile.empty?
File.expand_path("../../Gemfile", __FILE__)
end
def lockfile
lockfile =
case File.basename(gemfile)
when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
else "#{gemfile}.lock"
end
File.expand_path(lockfile)
end
def lockfile_version
return unless File.file?(lockfile)
lockfile_contents = File.read(lockfile)
return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
Regexp.last_match(1)
end
def bundler_requirement
@bundler_requirement ||=
env_var_version || cli_arg_version ||
bundler_requirement_for(lockfile_version)
end
def bundler_requirement_for(version)
return "#{Gem::Requirement.default}.a" unless version
bundler_gem_version = Gem::Version.new(version)
requirement = bundler_gem_version.approximate_recommendation
return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0")
requirement += ".a" if bundler_gem_version.prerelease?
requirement
end
def load_bundler!
ENV["BUNDLE_GEMFILE"] ||= gemfile
activate_bundler
end
def activate_bundler
gem_error = activation_error_handling do
gem "bundler", bundler_requirement
end
return if gem_error.nil?
require_error = activation_error_handling do
require "bundler/version"
end
return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
exit 42
end
def activation_error_handling
yield
nil
rescue StandardError, LoadError => e
e
end
end
m.load_bundler!
if m.invoked_as_script?
load Gem.bin_path("bundler", "bundle")
end

View file

@ -1,4 +0,0 @@
#!/usr/bin/env ruby
require_relative "../config/application"
require "importmap/commands"

View file

@ -1,4 +0,0 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path("../config/application", __dir__)
require_relative "../config/boot"
require "rails/commands"

View file

@ -1,4 +0,0 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require "rake"
Rake.application.run

View file

@ -1,33 +0,0 @@
#!/usr/bin/env ruby
require "fileutils"
# path to your application root.
APP_ROOT = File.expand_path("..", __dir__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
end
FileUtils.chdir APP_ROOT do
# This script is a way to set up or update your development environment automatically.
# This script is idempotent, so that you can run it at any time and get an expectable outcome.
# Add necessary setup steps to this file.
puts "== Installing dependencies =="
system! "gem install bundler --conservative"
system("bundle check") || system!("bundle install")
# puts "\n== Copying sample files =="
# unless File.exist?("config/database.yml")
# FileUtils.cp "config/database.yml.sample", "config/database.yml"
# end
puts "\n== Preparing database =="
system! "bin/rails db:prepare"
puts "\n== Removing old logs and tempfiles =="
system! "bin/rails log:clear tmp:clear"
puts "\n== Restarting application server =="
system! "bin/rails restart"
end

View file

@ -1,6 +0,0 @@
# This file is used by Rack-based servers to start the application.
require_relative "config/environment"
run Rails.application
Rails.application.load_server

View file

@ -1,36 +0,0 @@
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
Dotenv::Railtie.load
module Borders
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
config.hosts << "dev.j4.pm"
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
config.session_store :cookie_store, key: '_interslice_session'
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore, config.session_options
config.middleware.use OmniAuth::Builder do
provider :discord, ENV['DISCORD_CLIENT_ID'], ENV['DISCORD_CLIENT_SECRET'], scope: 'identify'
end
OmniAuth.config.logger = Rails.logger
end
end

View file

@ -1,4 +0,0 @@
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup" # Set up gems listed in the Gemfile.
require "bootsnap/setup" # Speed up boot time by caching expensive operations.

View file

@ -1,10 +0,0 @@
development:
adapter: async
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: borders_production

View file

@ -1 +0,0 @@
ZtWeNs0IpngwoBiDB0tKSzf4Yzcn2vdeq3ZF6oqiGFxk2mKOZhGbKz1UpS5p4jz1dAPuY9O9AAxCaW5FMFoF8Kt3XRVW3z+krmVrHmQRtMnRSc4CrqrP/W55R49gRcjthuVDQIlu05vo/Q1pVIMWjYTFeVbWNmyYM+Yfr2RS3pStLOqqW5jF8pczXnMj10Tuk7430kiNU+0mJKb8dDO2ONsygfpOJnyGDOri9IyMHtkuHqMYYoDrAhH7UQDD+Y1J4J9QA1LFmqiFksas9lA3sftYejBluKr8VoQBCFWiCrUaTtwOJPDPnkMn/ruiW/P/B98sQKvxFU2dZqtKn9Q1aYu02uV+ZcfTp9bfQN94F4U9KTfYFzUjanRv/pKIJO6MNU+70fguipnED47KL+v20Xu6l0/xlt+5QBjk--FW0lnsnDid5zpyui--hZ6fbB+07IFgNCd1el7bFg==

View file

@ -1,25 +0,0 @@
# SQLite. Versions 3.8.0 and up are supported.
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem "sqlite3"
#
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: db/test.sqlite3
production:
<<: *default
database: db/production.sqlite3

View file

@ -1,5 +0,0 @@
# Load the Rails application.
require_relative "application"
# Initialize the Rails application.
Rails.application.initialize!

View file

@ -1,70 +0,0 @@
require "active_support/core_ext/integer/time"
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded any time
# it changes. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
# Do not eager load code on boot.
config.eager_load = false
# Show full error reports.
config.consider_all_requests_local = true
# Enable server timing
config.server_timing = true
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join("tmp/caching-dev.txt").exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.cache_store = :memory_store
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{2.days.to_i}"
}
else
config.action_controller.perform_caching = false
config.cache_store = :null_store
end
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
config.action_mailer.perform_caching = false
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
# Suppress logger output for asset requests.
config.assets.quiet = true
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
end

View file

@ -1,93 +0,0 @@
require "active_support/core_ext/integer/time"
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
config.cache_classes = true
# Eager load code on boot. This eager loads most of Rails and
# your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true
# Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
# Compress CSS using a preprocessor.
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.asset_host = "http://assets.example.com"
# Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
# config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
# Mount Action Cable outside main process or domain.
# config.action_cable.mount_path = nil
# config.action_cable.url = "wss://example.com/cable"
# config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
# Include generic and useful information about system operation, but avoid logging too much
# information to avoid inadvertent exposure of personally identifiable information (PII).
config.log_level = :info
# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
# Use a real queuing backend for Active Job (and separate queues per environment).
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "borders_production"
config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Don't log any deprecations.
config.active_support.report_deprecations = false
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
# Use a different logger for distributed setups.
# require "syslog/logger"
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
end

View file

@ -1,60 +0,0 @@
require "active_support/core_ext/integer/time"
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Turn false under Spring and add config.action_view.cache_template_loading = true.
config.cache_classes = true
# Eager loading loads your whole application. When running a single test locally,
# this probably isn't necessary. It's a good idea to do in a continuous integration
# system, or in some way before deploying your code.
config.eager_load = ENV["CI"].present?
# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.enabled = true
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{1.hour.to_i}"
}
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.cache_store = :null_store
# Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
# Store uploaded files on the local file system in a temporary directory.
config.active_storage.service = :test
config.action_mailer.perform_caching = false
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
end

View file

@ -1,7 +0,0 @@
# Pin npm packages by running ./bin/importmap
pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"

View file

@ -1,12 +0,0 @@
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = "1.0"
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css )

View file

@ -1,26 +0,0 @@
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy
# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
# Rails.application.configure do
# config.content_security_policy do |policy|
# policy.default_src :self, :https
# policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
# policy.style_src :self, :https
# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
# end
#
# # Generate session nonces for permitted importmap and inline scripts
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
# config.content_security_policy_nonce_directives = %w(script-src)
#
# # Report CSP violations to a specified URI. See:
# # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# # config.content_security_policy_report_only = true
# end

View file

@ -1,8 +0,0 @@
# Be sure to restart your server when you modify this file.
# Configure parameters to be filtered from the log file. Use this to limit dissemination of
# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
# notations and behaviors.
Rails.application.config.filter_parameters += [
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
]

View file

@ -1,16 +0,0 @@
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, "\\1en"
# inflect.singular /^(ox)en/i, "\\1"
# inflect.irregular "person", "people"
# inflect.uncountable %w( fish sheep )
# end
# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym "RESTful"
# end

View file

@ -1,11 +0,0 @@
# Define an application-wide HTTP permissions policy. For further
# information see https://developers.google.com/web/updates/2018/06/feature-policy
#
# Rails.application.config.permissions_policy do |f|
# f.camera :none
# f.gyroscope :none
# f.microphone :none
# f.usb :none
# f.fullscreen :self
# f.payment :self, "https://secure.example.com"
# end

View file

@ -1,33 +0,0 @@
# Files in the config/locales directory are used for internationalization
# and are automatically loaded by Rails. If you want to use locales other
# than English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
# I18n.t "hello"
#
# In views, this is aliased to just `t`:
#
# <%= t("hello") %>
#
# To use a different locale, set it with `I18n.locale`:
#
# I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# The following keys must be escaped otherwise they will not be retrieved by
# the default I18n backend:
#
# true, false, on, off, yes, no
#
# Instead, surround them with single quotes.
#
# en:
# "true": "foo"
#
# To learn more, please read the Rails Internationalization guide
# available at https://guides.rubyonrails.org/i18n.html.
en:
hello: "Hello world"

View file

@ -1,43 +0,0 @@
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
# Specifies the `worker_timeout` threshold that Puma will use to wait before
# terminating a worker in development environments.
#
worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port ENV.fetch("PORT") { 3000 }
# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "development" }
# Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
# preload_app!
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart

View file

@ -1,12 +0,0 @@
Rails.application.routes.draw do
resources :users
resources :discord_users
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
# root "articles#index"
match "/login" => "sessions#new", :as => :login, via: [:get, :post]
match "/auth/:provider/callback" => "sessions#create", via: [:post]
match "/logout" => "sessions#destroy", :as => :logout, via: [:post]
match "/auth/failure" => "sessions#failure", via: [:get]
end

View file

@ -1,34 +0,0 @@
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("storage") %>
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
# service: S3
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
# region: us-east-1
# bucket: your_own_bucket-<%= Rails.env %>
# Remember not to checkin your GCS keyfile to a repository
# google:
# service: GCS
# project: your_project
# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
# bucket: your_own_bucket-<%= Rails.env %>
# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
# microsoft:
# service: AzureStorage
# storage_account_name: your_account_name
# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
# container: your_container_name-<%= Rails.env %>
# mirror:
# service: Mirror
# primary: local
# mirrors: [ amazon, google, microsoft ]

1
cypress.json Normal file
View file

@ -0,0 +1 @@
{}

6
cypress/.eslintrc.js Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: "./tsconfig.json",
},
};

48
cypress/e2e/smoke.ts Normal file
View file

@ -0,0 +1,48 @@
import faker from "@faker-js/faker";
describe("smoke tests", () => {
afterEach(() => {
cy.cleanupUser();
});
it("should allow you to register and login", () => {
const loginForm = {
email: `${faker.internet.userName()}@example.com`,
password: faker.internet.password(),
};
cy.then(() => ({ email: loginForm.email })).as("user");
cy.visit("/");
cy.findByRole("link", { name: /sign up/i }).click();
cy.findByRole("textbox", { name: /email/i }).type(loginForm.email);
cy.findByLabelText(/password/i).type(loginForm.password);
cy.findByRole("button", { name: /create account/i }).click();
cy.findByRole("link", { name: /notes/i }).click();
cy.findByRole("button", { name: /logout/i }).click();
cy.findByRole("link", { name: /log in/i });
});
it("should allow you to make a note", () => {
const testNote = {
title: faker.lorem.words(1),
body: faker.lorem.sentences(1),
};
cy.login();
cy.visit("/");
cy.findByRole("link", { name: /notes/i }).click();
cy.findByText("No notes yet");
cy.findByRole("link", { name: /\+ new note/i }).click();
cy.findByRole("textbox", { name: /title/i }).type(testNote.title);
cy.findByRole("textbox", { name: /body/i }).type(testNote.body);
cy.findByRole("button", { name: /save/i }).click();
cy.findByRole("button", { name: /delete/i }).click();
cy.findByText("No notes yet");
});
});

View file

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

25
cypress/plugins/index.ts Normal file
View file

@ -0,0 +1,25 @@
module.exports = (
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
) => {
const isDev = config.watchForFileChanges;
const port = process.env.PORT ?? (isDev ? "3000" : "8811");
const configOverrides: Partial<Cypress.PluginConfigOptions> = {
baseUrl: `http://localhost:${port}`,
integrationFolder: "cypress/e2e",
video: !process.env.CI,
screenshotOnRunFailure: !process.env.CI,
};
Object.assign(config, configOverrides);
// To use this:
// cy.task('log', whateverYouWantInTheTerminal)
on("task", {
log(message) {
console.log(message);
return null;
},
});
return config;
};

View file

@ -0,0 +1,77 @@
import faker from "@faker-js/faker";
declare global {
namespace Cypress {
interface Chainable {
/**
* Logs in with a random user. Yields the user and adds an alias to the user
*
* @returns {typeof login}
* @memberof Chainable
* @example
* cy.login()
* @example
* cy.login({ email: 'whatever@example.com' })
*/
login: typeof login;
/**
* Deletes the current @user
*
* @returns {typeof cleanupUser}
* @memberof Chainable
* @example
* cy.cleanupUser()
* @example
* cy.cleanupUser({ email: 'whatever@example.com' })
*/
cleanupUser: typeof cleanupUser;
}
}
}
function login({
email = faker.internet.email(undefined, undefined, "example.com"),
}: {
email?: string;
} = {}) {
cy.then(() => ({ email })).as("user");
cy.exec(
`node --require esbuild-register ./cypress/support/create-user.ts "${email}"`
).then(({ stdout }) => {
const cookieValue = stdout
.replace(/.*<cookie>(?<cookieValue>.*)<\/cookie>.*/s, "$<cookieValue>")
.trim();
cy.setCookie("__session", cookieValue);
});
return cy.get("@user");
}
function cleanupUser({ email }: { email?: string } = {}) {
if (email) {
deleteUserByEmail(email);
} else {
cy.get("@user").then((user) => {
const email = (user as { email?: string }).email;
if (email) {
deleteUserByEmail(email);
}
});
}
cy.clearCookie("__session");
}
function deleteUserByEmail(email: string) {
cy.exec(
`node --require esbuild-register ./cypress/support/delete-user.ts "${email}"`
);
cy.clearCookie("__session");
}
Cypress.Commands.add("login", login);
Cypress.Commands.add("cleanupUser", cleanupUser);
/*
eslint
@typescript-eslint/no-namespace: "off",
*/

View file

@ -0,0 +1,47 @@
// Use this to create a new user and login with that user
// Simply call this with:
// node --require esbuild-register ./cypress/support/create-user.ts username@example.com
// and it will log out the cookie value you can use to interact with the server
// as that new user.
import { parse } from "cookie";
import { installGlobals } from "@remix-run/node/globals";
import { createUserSession } from "~/session.server";
import { createUser } from "~/models/user.server";
installGlobals();
async function createAndLogin(email: string) {
if (!email) {
throw new Error("email required for login");
}
if (!email.endsWith("@example.com")) {
throw new Error("All test emails must end in @example.com");
}
const user = await createUser(email, "myreallystrongpassword");
const response = await createUserSession({
request: new Request(""),
userId: user.id,
remember: false,
redirectTo: "/",
});
const cookieValue = response.headers.get("Set-Cookie");
if (!cookieValue) {
throw new Error("Cookie missing from createUserSession response");
}
const parsedCookie = parse(cookieValue);
// we log it like this so our cypress command can parse it out and set it as
// the cookie value.
console.log(
`
<cookie>
${parsedCookie.__session}
</cookie>
`.trim()
);
}
createAndLogin(process.argv[2]);

View file

@ -0,0 +1,22 @@
// Use this to delete a user by their email
// Simply call this with:
// node --require esbuild-register ./cypress/support/delete-user.ts username@example.com
// and that user will get deleted
import { installGlobals } from "@remix-run/node/globals";
import { prisma } from "~/db.server";
installGlobals();
async function deleteUser(email: string) {
if (!email) {
throw new Error("email required for login");
}
if (!email.endsWith("@example.com")) {
throw new Error("All test emails must end in @example.com");
}
await prisma.user.delete({ where: { email } });
}
deleteUser(process.argv[2]);

2
cypress/support/index.ts Normal file
View file

@ -0,0 +1,2 @@
import "@testing-library/cypress/add-commands";
import "./commands";

31
cypress/tsconfig.json Normal file
View file

@ -0,0 +1,31 @@
{
"exclude": [
"../node_modules/@types/jest",
"../node_modules/@testing-library/jest-dom"
],
"include": [
"./index.ts",
"e2e/**/*",
"plugins/**/*",
"support/**/*",
"../node_modules/cypress",
"../node_modules/@testing-library/cypress"
],
"compilerOptions": {
"baseUrl": ".",
"noEmit": true,
"types": ["node", "cypress", "@testing-library/cypress"],
"esModuleInterop": true,
"jsx": "react",
"moduleResolution": "node",
"target": "es2019",
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"typeRoots": ["../types", "../node_modules/@types"],
"paths": {
"~/*": ["../app/*"]
}
}
}

View file

@ -1,12 +0,0 @@
class CreateDiscordusers < ActiveRecord::Migration[7.0]
def change
create_table :discord_users do |t|
t.string :discord_id
t.string :username
t.string :discriminator
t.string :avatar
t.timestamps
end
end
end

View file

@ -1,11 +0,0 @@
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :provider
t.string :uid
t.string :border
t.timestamps
end
end
end

View file

@ -1,30 +0,0 @@
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2022_04_09_220708) do
create_table "discord_users", force: :cascade do |t|
t.string "discord_id"
t.string "username"
t.string "discriminator"
t.string "avatar"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "users", force: :cascade do |t|
t.string "provider"
t.string "uid"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end

View file

@ -1,7 +0,0 @@
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
#
# Examples:
#
# movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }])
# Character.create(name: "Luke", movie: movies.first)

13
docker-compose.yml Normal file
View file

@ -0,0 +1,13 @@
version: "3.7"
services:
postgres:
image: postgres:latest
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- "5432:5432"
volumes:
- ./postgres-data:/var/lib/postgresql/data

50
fly.toml Normal file
View file

@ -0,0 +1,50 @@
app = "border-server-a1df"
kill_signal = "SIGINT"
kill_timeout = 5
processes = [ ]
[env]
PORT = "8080"
[deploy]
release_command = "npx prisma migrate deploy"
[experimental]
allowed_public_ports = [ ]
auto_rollback = true
[[services]]
internal_port = 8_080
processes = [ "app" ]
protocol = "tcp"
script_checks = [ ]
[services.concurrency]
hard_limit = 25
soft_limit = 20
type = "connections"
[[services.ports]]
handlers = [ "http" ]
port = 80
force_https = true
[[services.ports]]
handlers = [ "tls", "http" ]
port = 443
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"
[[services.http_checks]]
interval = 10_000
grace_period = "5s"
method = "get"
path = "/healthcheck"
protocol = "http"
timeout = 2_000
tls_skip_verify = false
headers = { }

View file

Some files were not shown because too many files have changed in this diff Show more