Compare commits
No commits in common. "2696e1f184ffcbaacfaa52d4f5468aee584e8b4f" and "83bc98115225524a492a092582a2f0a03bc8bad8" have entirely different histories.
2696e1f184
...
83bc981152
175 changed files with 21332 additions and 1803 deletions
19
.eslintrc.js
Normal file
19
.eslintrc.js
Normal 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
7
.gitattributes
vendored
|
@ -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
43
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
system
|
53
Dockerfile
Normal file
53
Dockerfile
Normal 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
80
Gemfile
|
@ -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
|
268
Gemfile.lock
268
Gemfile.lock
|
@ -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
199
README.md
|
@ -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.
|
||||
|
|
6
Rakefile
6
Rakefile
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
//= link_tree ../images
|
||||
//= link_directory ../stylesheets .css
|
||||
//= link_tree ../../javascript .js
|
||||
//= link_tree ../../../vendor/javascript .js
|
|
@ -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
|
||||
*/
|
|
@ -1,4 +0,0 @@
|
|||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
end
|
|
@ -1,4 +0,0 @@
|
|||
module ApplicationCable
|
||||
class Connection < ActionCable::Connection::Base
|
||||
end
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
class ApplicationController < ActionController::Base
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
class DiscordUsersController < ApplicationController
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
class SessionsController < ApplicationController
|
||||
end
|
|
@ -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
62
app/db.server.ts
Normal 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
124
app/discord/index.ts
Normal 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
4
app/entry.client.tsx
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { hydrate } from "react-dom";
|
||||
import { RemixBrowser } from "remix";
|
||||
|
||||
hydrate(<RemixBrowser />, document);
|
21
app/entry.server.tsx
Normal file
21
app/entry.server.tsx
Normal 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,
|
||||
});
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
module ApplicationHelper
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
module DiscordUsersHelper
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
module SessionsHelper
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
module UsersHelper
|
||||
end
|
|
@ -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"
|
|
@ -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 }
|
|
@ -1,7 +0,0 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.element.textContent = "Hello World!"
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: "from@example.com"
|
||||
layout "mailer"
|
||||
end
|
|
@ -1,3 +0,0 @@
|
|||
class ApplicationRecord < ActiveRecord::Base
|
||||
primary_abstract_class
|
||||
end
|
|
@ -1,4 +0,0 @@
|
|||
class DiscordUser < ApplicationRecord
|
||||
attr_accessible :discord_id, :username, :discriminator, :avatar
|
||||
validates :discord_id, :uniqueness => true
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
class User < ApplicationRecord
|
||||
end
|
65
app/models/user.server.ts
Normal file
65
app/models/user.server.ts
Normal 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
40
app/root.tsx
Normal 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>
|
||||
);
|
||||
}
|
23
app/routes/healthcheck.tsx
Normal file
23
app/routes/healthcheck.tsx
Normal 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
23
app/routes/index.tsx
Normal 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
39
app/routes/login.tsx
Normal 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
25
app/routes/loginstart.tsx
Normal 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
11
app/routes/logout.tsx
Normal 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
97
app/session.server.ts
Normal 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
44
app/utils.ts
Normal 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;
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -1 +0,0 @@
|
|||
<%= yield %>
|
|
@ -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 %>
|
|
@ -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>
|
|
@ -1,2 +0,0 @@
|
|||
json.extract! user, :id, :provider, :uid, :border, :created_at, :updated_at
|
||||
json.url user_url(user, format: :json)
|
|
@ -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>
|
|
@ -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 %>
|
|
@ -1 +0,0 @@
|
|||
json.array! @users, partial: "users/user", as: :user
|
|
@ -1,9 +0,0 @@
|
|||
<h1>New user</h1>
|
||||
|
||||
<%= render "form", user: @user %>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<%= link_to "Back to users", users_path %>
|
||||
</div>
|
|
@ -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>
|
|
@ -1 +0,0 @@
|
|||
json.partial! "users/user", user: @user
|
114
bin/bundle
114
bin/bundle
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require_relative "../config/application"
|
||||
require "importmap/commands"
|
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/env ruby
|
||||
APP_PATH = File.expand_path("../config/application", __dir__)
|
||||
require_relative "../config/boot"
|
||||
require "rails/commands"
|
4
bin/rake
4
bin/rake
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/env ruby
|
||||
require_relative "../config/boot"
|
||||
require "rake"
|
||||
Rake.application.run
|
33
bin/setup
33
bin/setup
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
ZtWeNs0IpngwoBiDB0tKSzf4Yzcn2vdeq3ZF6oqiGFxk2mKOZhGbKz1UpS5p4jz1dAPuY9O9AAxCaW5FMFoF8Kt3XRVW3z+krmVrHmQRtMnRSc4CrqrP/W55R49gRcjthuVDQIlu05vo/Q1pVIMWjYTFeVbWNmyYM+Yfr2RS3pStLOqqW5jF8pczXnMj10Tuk7430kiNU+0mJKb8dDO2ONsygfpOJnyGDOri9IyMHtkuHqMYYoDrAhH7UQDD+Y1J4J9QA1LFmqiFksas9lA3sftYejBluKr8VoQBCFWiCrUaTtwOJPDPnkMn/ruiW/P/B98sQKvxFU2dZqtKn9Q1aYu02uV+ZcfTp9bfQN94F4U9KTfYFzUjanRv/pKIJO6MNU+70fguipnED47KL+v20Xu6l0/xlt+5QBjk--FW0lnsnDid5zpyui--hZ6fbB+07IFgNCd1el7bFg==
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
# Load the Rails application.
|
||||
require_relative "application"
|
||||
|
||||
# Initialize the Rails application.
|
||||
Rails.application.initialize!
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -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 )
|
|
@ -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
|
|
@ -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
|
||||
]
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
|
@ -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
1
cypress.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
6
cypress/.eslintrc.js
Normal file
6
cypress/.eslintrc.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
};
|
48
cypress/e2e/smoke.ts
Normal file
48
cypress/e2e/smoke.ts
Normal 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");
|
||||
});
|
||||
});
|
5
cypress/fixtures/example.json
Normal file
5
cypress/fixtures/example.json
Normal 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
25
cypress/plugins/index.ts
Normal 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;
|
||||
};
|
77
cypress/support/commands.ts
Normal file
77
cypress/support/commands.ts
Normal 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",
|
||||
*/
|
47
cypress/support/create-user.ts
Normal file
47
cypress/support/create-user.ts
Normal 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]);
|
22
cypress/support/delete-user.ts
Normal file
22
cypress/support/delete-user.ts
Normal 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
2
cypress/support/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
import "@testing-library/cypress/add-commands";
|
||||
import "./commands";
|
31
cypress/tsconfig.json
Normal file
31
cypress/tsconfig.json
Normal 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/*"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
30
db/schema.rb
30
db/schema.rb
|
@ -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
|
|
@ -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
13
docker-compose.yml
Normal 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
50
fly.toml
Normal 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 = { }
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue