Compare commits

..

No commits in common. "13992e1dbe79ed73e19a790c7ee231e13b4b1098" and "54bdd52e7588c163c7ee0b9691357bab86bb32c0" have entirely different histories.

50 changed files with 5 additions and 1337 deletions

2
.gitignore vendored
View file

@ -30,5 +30,3 @@
# Ignore master key for decrypting credentials and more. # Ignore master key for decrypting credentials and more.
/config/master.key /config/master.key
.DS_Store

View file

@ -41,7 +41,6 @@
<orderEntry type="library" scope="PROVIDED" name="activesupport (v8.0.5, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="activesupport (v8.0.5, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="TEST" name="annotaterb (v4.22.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="TEST" name="annotaterb (v4.22.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ast (v2.4.3, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="ast (v2.4.3, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="backport (v1.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="base64 (v0.3.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="base64 (v0.3.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="bcrypt (v3.1.21, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="bcrypt (v3.1.21, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="bcrypt_pbkdf (v1.1.2, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="bcrypt_pbkdf (v1.1.2, RVM: ruby-3.4.4) [gem]" level="application" />
@ -57,7 +56,6 @@
<orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="date (v3.5.1, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="date (v3.5.1, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="debug (v1.11.1, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="debug (v1.11.1, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="diff-lcs (v1.6.2, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="dotenv (v3.2.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="dotenv (v3.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="TEST" name="dotenv-rails (v3.2.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="TEST" name="dotenv-rails (v3.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="drb (v2.2.3, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="drb (v2.2.3, RVM: ruby-3.4.4) [gem]" level="application" />
@ -70,11 +68,8 @@
<orderEntry type="library" scope="PROVIDED" name="i18n (v1.14.8, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="i18n (v1.14.8, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.2, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.2, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="irb (v1.18.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="irb (v1.18.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="jaro_winkler (v1.6.1, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="json (v2.19.5, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="json (v2.19.5, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="kamal (v2.11.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="kamal (v2.11.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="kramdown (v2.5.1, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="kramdown-parser-gfm (v1.1.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.5, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.5, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="lint_roller (v1.1.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="lint_roller (v1.1.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="logger (v1.7.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="logger (v1.7.0, RVM: ruby-3.4.4) [gem]" level="application" />
@ -93,7 +88,6 @@
<orderEntry type="library" scope="PROVIDED" name="net-ssh (v7.3.2, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="net-ssh (v7.3.2, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="nio4r (v2.7.5, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="nio4r (v2.7.5, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="nokogiri (v1.19.3, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="nokogiri (v1.19.3, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="observer (v0.1.2, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ostruct (v0.6.3, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="ostruct (v0.6.3, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="parallel (v2.1.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="parallel (v2.1.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="parser (v3.3.11.1, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="parser (v3.3.11.1, RVM: ruby-3.4.4) [gem]" level="application" />
@ -116,11 +110,9 @@
<orderEntry type="library" scope="PROVIDED" name="railties (v8.0.5, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="railties (v8.0.5, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rake (v13.4.2, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="rake (v13.4.2, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rbs (v3.9.5, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rdoc (v7.2.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="rdoc (v7.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.12.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.12.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="reline (v0.6.3, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="reline (v0.6.3, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="reverse_markdown (v3.0.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rexml (v3.4.4, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="rexml (v3.4.4, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rubocop (v1.86.2, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="rubocop (v1.86.2, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.49.1, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.49.1, RVM: ruby-3.4.4) [gem]" level="application" />
@ -130,7 +122,6 @@
<orderEntry type="library" scope="PROVIDED" name="ruby-graphviz (v1.2.5, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="ruby-graphviz (v1.2.5, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="TEST" name="solargraph (v0.57.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="solid_cable (v3.0.12, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="solid_cable (v3.0.12, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="solid_cache (v1.0.10, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="solid_cache (v1.0.10, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="solid_queue (v1.4.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="solid_queue (v1.4.0, RVM: ruby-3.4.4) [gem]" level="application" />
@ -138,7 +129,6 @@
<orderEntry type="library" scope="PROVIDED" name="stringio (v3.2.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="stringio (v3.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="thor (v1.5.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="thor (v1.5.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="thruster (v0.1.20, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="thruster (v0.1.20, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="tilt (v2.6.1, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="timeout (v0.6.1, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="timeout (v0.6.1, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="tsort (v0.2.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="tsort (v0.2.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="tzinfo (v2.0.6, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="tzinfo (v2.0.6, RVM: ruby-3.4.4) [gem]" level="application" />
@ -148,9 +138,6 @@
<orderEntry type="library" scope="PROVIDED" name="useragent (v0.16.11, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="useragent (v0.16.11, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="websocket-driver (v0.8.0, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="websocket-driver (v0.8.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="websocket-extensions (v0.1.5, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="websocket-extensions (v0.1.5, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="yard (v0.9.37, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="yard-activesupport-concern (v0.0.1, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="yard-solargraph (v0.1.0, RVM: ruby-3.4.4) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="zeitwerk (v2.7.5, RVM: ruby-3.4.4) [gem]" level="application" /> <orderEntry type="library" scope="PROVIDED" name="zeitwerk (v2.7.5, RVM: ruby-3.4.4) [gem]" level="application" />
</component> </component>
<component name="RModuleSettingsStorage"> <component name="RModuleSettingsStorage">

View file

@ -49,7 +49,4 @@ group :development, :test do
gem "annotaterb" gem "annotaterb"
gem "dotenv-rails" gem "dotenv-rails"
# Ruby language server for IDE support
gem "solargraph", require: false
end end

View file

@ -76,7 +76,6 @@ GEM
activerecord (>= 6.0.0) activerecord (>= 6.0.0)
activesupport (>= 6.0.0) activesupport (>= 6.0.0)
ast (2.4.3) ast (2.4.3)
backport (1.2.0)
base64 (0.3.0) base64 (0.3.0)
bcrypt (3.1.21) bcrypt (3.1.21)
bcrypt_pbkdf (1.1.2) bcrypt_pbkdf (1.1.2)
@ -95,7 +94,6 @@ GEM
debug (1.11.1) debug (1.11.1)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
diff-lcs (1.6.2)
dotenv (3.2.0) dotenv (3.2.0)
dotenv-rails (3.2.0) dotenv-rails (3.2.0)
dotenv (= 3.2.0) dotenv (= 3.2.0)
@ -119,7 +117,6 @@ GEM
prism (>= 1.3.0) prism (>= 1.3.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jaro_winkler (1.6.1)
json (2.19.5) json (2.19.5)
kamal (2.11.0) kamal (2.11.0)
activesupport (>= 7.0) activesupport (>= 7.0)
@ -132,10 +129,6 @@ GEM
sshkit (>= 1.23.0, < 2.0) sshkit (>= 1.23.0, < 2.0)
thor (~> 1.3) thor (~> 1.3)
zeitwerk (>= 2.6.18, < 3.0) zeitwerk (>= 2.6.18, < 3.0)
kramdown (2.5.1)
rexml (>= 3.3.9)
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
lint_roller (1.1.0) lint_roller (1.1.0)
logger (1.7.0) logger (1.7.0)
@ -183,7 +176,6 @@ GEM
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.3-x86_64-linux-musl) nokogiri (1.19.3-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
observer (0.1.2)
ostruct (0.6.3) ostruct (0.6.3)
parallel (2.1.0) parallel (2.1.0)
parser (3.3.11.1) parser (3.3.11.1)
@ -251,8 +243,6 @@ GEM
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.4.2) rake (13.4.2)
rbs (3.9.5)
logger
rdoc (7.2.0) rdoc (7.2.0)
erb erb
psych (>= 4.0.0) psych (>= 4.0.0)
@ -260,8 +250,6 @@ GEM
regexp_parser (2.12.0) regexp_parser (2.12.0)
reline (0.6.3) reline (0.6.3)
io-console (~> 0.5) io-console (~> 0.5)
reverse_markdown (3.0.0)
nokogiri
rexml (3.4.4) rexml (3.4.4)
rubocop (1.86.2) rubocop (1.86.2)
json (~> 2.3) json (~> 2.3)
@ -295,27 +283,6 @@ GEM
rexml rexml
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
securerandom (0.4.1) securerandom (0.4.1)
solargraph (0.57.0)
backport (~> 1.2)
benchmark (~> 0.4)
bundler (~> 2.0)
diff-lcs (~> 1.4)
jaro_winkler (~> 1.6, >= 1.6.1)
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1)
logger (~> 1.6)
observer (~> 0.1)
ostruct (~> 0.6)
parser (~> 3.0)
prism (~> 1.4)
rbs (>= 3.6.1, <= 4.0.0.dev.4)
reverse_markdown (~> 3.0)
rubocop (~> 1.76)
thor (~> 1.0)
tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24)
yard-activesupport-concern (~> 0.0)
yard-solargraph (~> 0.1)
solid_cable (3.0.12) solid_cable (3.0.12)
actioncable (>= 7.2) actioncable (>= 7.2)
activejob (>= 7.2) activejob (>= 7.2)
@ -345,7 +312,6 @@ GEM
thruster (0.1.20-aarch64-linux) thruster (0.1.20-aarch64-linux)
thruster (0.1.20-arm64-darwin) thruster (0.1.20-arm64-darwin)
thruster (0.1.20-x86_64-linux) thruster (0.1.20-x86_64-linux)
tilt (2.6.1)
timeout (0.6.1) timeout (0.6.1)
tsort (0.2.0) tsort (0.2.0)
tzinfo (2.0.6) tzinfo (2.0.6)
@ -359,11 +325,6 @@ GEM
base64 base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
yard (0.9.37)
yard-activesupport-concern (0.0.1)
yard (>= 0.8)
yard-solargraph (0.1.0)
yard (~> 0.9)
zeitwerk (2.7.5) zeitwerk (2.7.5)
PLATFORMS PLATFORMS
@ -390,7 +351,6 @@ DEPENDENCIES
rails (~> 8.0.4) rails (~> 8.0.4)
rails-erd rails-erd
rubocop-rails-omakase rubocop-rails-omakase
solargraph
solid_cable solid_cable
solid_cache solid_cache
solid_queue solid_queue

View file

@ -1,26 +1,15 @@
class ApplicationController < ActionController::API class ApplicationController < ActionController::API
include Renderable
before_action :authenticate! before_action :authenticate!
rescue_from Unauthorized do
render_error("Unauthorized", :unauthorized)
end
private private
def authenticate! def authenticate!
token = request.headers["Authorization"]&.split(" ")&.last token = request.headers["Authorization"]&.split(" ")&.last
@current_session = Session.find_by(token: token) @current_user = User.find_by(token:)
raise Unauthorized unless @current_session
@current_user = @current_session.user render json: { error: "Unauthorized" }, status: :unauthorized unless @current_user
end end
def current_user def current_user
@current_user @current_user
end end
def current_session
@current_session
end
end end

View file

@ -1,19 +0,0 @@
# frozen_string_literal: true
module Renderable
extend ActiveSupport::Concern
def render_success(data = nil, status = :ok, meta = {})
body = { success: true }
body[:data] = data if data
body[:meta] = meta if meta.present?
render json: body, status:
end
def render_error(message, status = :unprocessable_entity, details: nil)
body = { success: false, error: { message: } }
body[:error][:details] = details if details.present?
render json: body, status:
end
end

View file

@ -1,26 +0,0 @@
class SessionsController < ApplicationController
skip_before_action :authenticate!, only: [ :create ]
def create
user = User.find_by(email: login_params[:email])
unless user.present? && user.authenticate(login_params[:password])
raise Unauthorized
end
session = user.sessions.create!
render_success(user: user, token: session.token)
end
def destroy
@current_session.destroy!
render_success(nil)
end
private
def login_params
params.permit(:email, :password)
end
end

View file

@ -1,28 +0,0 @@
class UsersController < ApplicationController
skip_before_action :authenticate!, only: [ :create ]
def create
user = User.new(user_params)
if user.save
session = user.sessions.create!
render_success({ user: user, token: session.token }, :created)
else
render_error("Failed to create user", :unprocessable_entity, details: user.errors.full_messages)
end
end
def show
end
def destroy
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
end

View file

@ -1 +0,0 @@
class Unauthorized < StandardError; end

View file

@ -1,25 +0,0 @@
# == Schema Information
#
# Table name: sessions
#
# id :bigint not null, primary key
# ip_address :string
# token :string
# user_agent :string
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
#
# Indexes
#
# index_sessions_on_token (token) UNIQUE
# index_sessions_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
#
class Session < ApplicationRecord
has_secure_token
belongs_to :user
end

View file

@ -18,5 +18,4 @@ class User < ApplicationRecord
has_secure_password has_secure_password
has_many :price_reports, dependent: :nullify has_many :price_reports, dependent: :nullify
has_many :moderation_logs, foreign_key: :admin_id, dependent: :nullify has_many :moderation_logs, foreign_key: :admin_id, dependent: :nullify
has_many :sessions, dependent: :destroy
end end

View file

@ -1,35 +0,0 @@
meta {
name: Login - Invalid Credentials
type: http
seq: 4
}
post {
url: {{baseUrl}}/login
}
headers {
Content-Type: application/json
}
body:json {
{
"email": "nonexistent@example.com",
"password": "wrongpassword"
}
}
assert {
res.status: equals 401
res.body.success: equals false
}
tests {
test("Invalid login returns error envelope", function() {
const data = res.getBody();
expect(data).to.have.property("success");
expect(data.success).to.equal(false);
expect(data).to.have.property("error");
expect(data.error).to.have.property("message");
});
}

View file

@ -1,64 +0,0 @@
meta {
name: Login
type: http
seq: 2
}
post {
url: {{baseUrl}}/login
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"email": "{{userEmail}}",
"password": "password123"
}
}
assert {
res.status: 200
res.body.success: true
res("data.token"): isNotEmpty
}
script:post-response {
const body = res.getBody();
if (body && body.data && body.data.token) {
bru.setVar("authToken", body.data.token);
bru.setVar("userId", body.data.user.id);
}
}
tests {
test("Login returns wrapped success response", function() {
const data = res.getBody();
expect(data).to.have.property("success");
expect(data.success).to.equal(true);
expect(data).to.have.property("data");
});
test("Data contains user object and token string", function() {
const data = res.getBody();
expect(data.data).to.have.property("user");
expect(data.data).to.have.property("token");
});
test("User object has correct shape", function() {
const data = res.getBody();
expect(data.data.user).to.have.property("id");
expect(data.data.user).to.have.property("email");
expect(data.data.user).to.have.property("name");
});
test("Token is a non-empty string", function() {
const data = res.getBody();
expect(data.data.token).to.be.a("string");
expect(data.data.token.length).to.be.greaterThan(0);
});
}

View file

@ -1,38 +0,0 @@
meta {
name: Logout
type: http
seq: 3
}
delete {
url: {{baseUrl}}/logout
body: none
auth: none
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 200
res.body.success: equals true
}
script:post-response {
bru.deleteVar("authToken");
bru.deleteVar("userId");
}
tests {
test("Logout returns { success: true }", function() {
const data = res.getBody();
expect(data).to.have.property("success");
expect(data.success).to.equal(true);
});
test("Logout response has no data envelope", function() {
const data = res.getBody();
expect(data).to.not.have.property("data");
});
}

View file

@ -1,60 +0,0 @@
meta {
name: Signup
type: http
seq: 1
}
post {
url: {{baseUrl}}/signup
body: json
auth: none
}
headers {
Content-Type: application/json
}
body:json {
{
"user": {
"name": "Test User",
"email": "test@example.com",
"password": "password123",
"password_confirmation": "password123"
}
}
}
assert {
res.status: equals 200
}
script:post-response {
const body = res.getBody();
if (body && body.data && body.data.token) {
bru.setVar("authToken", body.data.token);
bru.setVar("userId", body.data.user.id);
bru.setVar("userEmail", "test@example.com");
}
}
tests {
test("Signup returns wrapped success response", function() {
const data = res.getBody();
expect(data).to.have.property("success");
expect(data.success).to.equal(true);
});
test("Signup response contains data envelope", function() {
const data = res.getBody();
expect(data).to.have.property("data");
});
test("Data contains user and token", function() {
const data = res.getBody();
expect(data.data).to.have.property("user");
expect(data.data).to.have.property("token");
expect(data.data.user).to.have.property("id");
expect(data.data.user).to.have.property("email");
});
}

View file

@ -1,28 +0,0 @@
meta {
name: Unauthorized Access
type: http
seq: 5
}
get {
url: {{baseUrl}}/users/{{userId}}
}
headers {
Authorization: Bearer invalid_token_here
}
assert {
res.status: equals 401
res.body.success: equals false
}
tests {
test("Invalid token returns 401 with error shape", function() {
const data = res.getBody();
expect(data).to.have.property("success");
expect(data.success).to.equal(false);
expect(data).to.have.property("error");
expect(data.error).to.have.property("message");
});
}

View file

@ -1,8 +0,0 @@
{
"version": "1",
"name": "Price Point API",
"type": "collection",
"ignore": [
"node_modules"
]
}

View file

@ -1,49 +0,0 @@
meta {
name: Create Chain
type: http
seq: 2
}
post {
url: {{baseUrl}}/chains
body: json
}
headers {
Content-Type: application/json
Authorization: Bearer {{authToken}}
}
body:json {
{
"chain": {
"name": "Test Chain Store"
}
}
}
assert {
res.status: equals 201
}
script:post-response {
const body = res.getBody();
if (body && body.id) {
bru.setVar("chainId", body.id);
}
}
tests {
test("Create chain returns chain object directly", function() {
const data = res.getBody();
expect(data).to.have.property("id");
expect(data).to.have.property("name");
expect(data).to.have.property("created_at");
expect(data).to.have.property("updated_at");
});
test("Created chain has correct name", function() {
const data = res.getBody();
expect(data.name).to.equal("Test Chain Store");
});
}

View file

@ -1,23 +0,0 @@
meta {
name: Delete Chain
type: http
seq: 5
}
delete {
url: {{baseUrl}}/chains/{{chainId}}
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 204
}
tests {
test("Delete chain returns 204 no content", function() {
expect(res.getStatus()).to.equal(204);
});
}

View file

@ -1,32 +0,0 @@
meta {
name: Get Chain
type: http
seq: 3
}
get {
url: {{baseUrl}}/chains/{{chainId}}
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 200
}
tests {
test("Get chain returns chain object directly", function() {
const data = res.getBody();
expect(data).to.have.property("id");
expect(data).to.have.property("name");
expect(data).to.have.property("created_at");
expect(data).to.have.property("updated_at");
});
test("Chain id matches requested id", function() {
const data = res.getBody();
expect(data.id).to.equal(Number(bru.getVar("chainId")));
});
}

View file

@ -1,32 +0,0 @@
meta {
name: List Chains
type: http
seq: 1
}
get {
url: {{baseUrl}}/chains
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 200
}
tests {
test("List chains returns an array", function() {
const data = res.getBody();
expect(data).to.be.an("array");
});
test("Each chain has correct shape", function() {
const data = res.getBody();
data.forEach(function(chain) {
expect(chain).to.have.property("id");
expect(chain).to.have.property("name");
});
});
}

View file

@ -1,36 +0,0 @@
meta {
name: Update Chain
type: http
seq: 4
}
patch {
url: {{baseUrl}}/chains/{{chainId}}
body: json
}
headers {
Content-Type: application/json
Authorization: Bearer {{authToken}}
}
body:json {
{
"chain": {
"name": "Updated Chain Name"
}
}
}
assert {
res.status: equals 200
}
tests {
test("Update chain returns updated object", function() {
const data = res.getBody();
expect(data).to.have.property("id");
expect(data).to.have.property("name");
expect(data.name).to.equal("Updated Chain Name");
});
}

View file

@ -1,22 +0,0 @@
meta {
name: Price Point API
}
headers {
Content-Type: application/json
}
tests {
test("Response status is not a server error", function() {
expect(res.getStatus()).to.be.below(500);
});
test("Response time is under 2 seconds", function() {
expect(res.getResponseTime()).to.be.below(2000);
});
test("Response body is valid JSON", function() {
const body = res.getBody();
expect(body).to.not.be.undefined;
});
}

View file

@ -1,4 +0,0 @@
vars {
baseUrl: http://localhost:3000
userEmail: test@example.com
}

View file

@ -1,57 +0,0 @@
meta {
name: Create Price Report
type: http
seq: 2
}
post {
url: {{baseUrl}}/products/{{productId}}/price_reports
body: json
}
headers {
Content-Type: application/json
Authorization: Bearer {{authToken}}
}
body:json {
{
"price_report": {
"price": "9.99",
"discount_price": "7.99",
"store_id": {{storeId}}
}
}
}
assert {
res.status: equals 201
}
script:post-response {
const body = res.getBody();
if (body && body.id) {
bru.setVar("priceReportId", body.id);
}
}
tests {
test("Create price report returns report object directly", function() {
const data = res.getBody();
expect(data).to.have.property("id");
expect(data).to.have.property("price");
expect(data).to.have.property("discount_price");
expect(data).to.have.property("store_id");
expect(data).to.have.property("product_id");
expect(data).to.have.property("user_id");
expect(data).to.have.property("reported_at");
expect(data).to.have.property("created_at");
expect(data).to.have.property("updated_at");
});
test("Price report has correct values", function() {
const data = res.getBody();
expect(data.product_id).to.equal(Number(bru.getVar("productId")));
expect(data.store_id).to.equal(Number(bru.getVar("storeId")));
});
}

View file

@ -1,23 +0,0 @@
meta {
name: Delete Price Report
type: http
seq: 5
}
delete {
url: {{baseUrl}}/price_reports/{{priceReportId}}
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 204
}
tests {
test("Delete price report returns 204 no content", function() {
expect(res.getStatus()).to.equal(204);
});
}

View file

@ -1,37 +0,0 @@
meta {
name: Get Price Report
type: http
seq: 3
}
get {
url: {{baseUrl}}/price_reports/{{priceReportId}}
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 200
}
tests {
test("Get price report returns report object directly", function() {
const data = res.getBody();
expect(data).to.have.property("id");
expect(data).to.have.property("price");
expect(data).to.have.property("discount_price");
expect(data).to.have.property("store_id");
expect(data).to.have.property("product_id");
expect(data).to.have.property("user_id");
expect(data).to.have.property("reported_at");
expect(data).to.have.property("created_at");
expect(data).to.have.property("updated_at");
});
test("Price report id matches requested id", function() {
const data = res.getBody();
expect(data.id).to.equal(Number(bru.getVar("priceReportId")));
});
}

View file

@ -1,35 +0,0 @@
meta {
name: List Price Reports
type: http
seq: 1
}
get {
url: {{baseUrl}}/products/{{productId}}/price_reports
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 200
}
tests {
test("List price reports returns an array", function() {
const data = res.getBody();
expect(data).to.be.an("array");
});
test("Each price report has correct shape", function() {
const data = res.getBody();
data.forEach(function(report) {
expect(report).to.have.property("id");
expect(report).to.have.property("price");
expect(report).to.have.property("store_id");
expect(report).to.have.property("product_id");
expect(report).to.have.property("user_id");
});
});
}

View file

@ -1,43 +0,0 @@
meta {
name: Update Price Report
type: http
seq: 4
}
patch {
url: {{baseUrl}}/price_reports/{{priceReportId}}
body: json
}
headers {
Content-Type: application/json
Authorization: Bearer {{authToken}}
}
body:json {
{
"price_report": {
"price": "12.99",
"discount_price": "10.99"
}
}
}
assert {
res.status: equals 200
}
tests {
test("Update price report returns updated object", function() {
const data = res.getBody();
expect(data).to.have.property("id");
expect(data).to.have.property("price");
expect(data).to.have.property("discount_price");
});
test("Updated values are reflected", function() {
const data = res.getBody();
expect(String(data.price)).to.equal("12.99");
expect(String(data.discount_price)).to.equal("10.99");
});
}

View file

@ -1,52 +0,0 @@
meta {
name: Create Product
type: http
seq: 2
}
post {
url: {{baseUrl}}/products
body: json
}
headers {
Content-Type: application/json
Authorization: Bearer {{authToken}}
}
body:json {
{
"product": {
"name": "Test Product",
"barcode": "1234567890123"
}
}
}
assert {
res.status: equals 201
}
script:post-response {
const body = res.getBody();
if (body && body.id) {
bru.setVar("productId", body.id);
}
}
tests {
test("Create product returns product object directly", function() {
const data = res.getBody();
expect(data).to.have.property("id");
expect(data).to.have.property("name");
expect(data).to.have.property("barcode");
expect(data).to.have.property("created_at");
expect(data).to.have.property("updated_at");
});
test("Created product has correct values", function() {
const data = res.getBody();
expect(data.name).to.equal("Test Product");
expect(data.barcode).to.equal("1234567890123");
});
}

View file

@ -1,23 +0,0 @@
meta {
name: Delete Product
type: http
seq: 5
}
delete {
url: {{baseUrl}}/products/{{productId}}
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 204
}
tests {
test("Delete product returns 204 no content", function() {
expect(res.getStatus()).to.equal(204);
});
}

View file

@ -1,33 +0,0 @@
meta {
name: Get Product
type: http
seq: 3
}
get {
url: {{baseUrl}}/products/{{productId}}
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 200
}
tests {
test("Get product returns product object directly", function() {
const data = res.getBody();
expect(data).to.have.property("id");
expect(data).to.have.property("name");
expect(data).to.have.property("barcode");
expect(data).to.have.property("created_at");
expect(data).to.have.property("updated_at");
});
test("Product id matches requested id", function() {
const data = res.getBody();
expect(data.id).to.equal(Number(bru.getVar("productId")));
});
}

View file

@ -1,33 +0,0 @@
meta {
name: List Products
type: http
seq: 1
}
get {
url: {{baseUrl}}/products
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 200
}
tests {
test("List products returns an array", function() {
const data = res.getBody();
expect(data).to.be.an("array");
});
test("Each product has correct shape", function() {
const data = res.getBody();
data.forEach(function(product) {
expect(product).to.have.property("id");
expect(product).to.have.property("name");
expect(product).to.have.property("barcode");
});
});
}

View file

@ -1,37 +0,0 @@
meta {
name: Update Product
type: http
seq: 4
}
patch {
url: {{baseUrl}}/products/{{productId}}
body: json
}
headers {
Content-Type: application/json
Authorization: Bearer {{authToken}}
}
body:json {
{
"product": {
"name": "Updated Product Name"
}
}
}
assert {
res.status: equals 200
}
tests {
test("Update product returns updated object", function() {
const data = res.getBody();
expect(data).to.have.property("id");
expect(data).to.have.property("name");
expect(data).to.have.property("barcode");
expect(data.name).to.equal("Updated Product Name");
});
}

View file

@ -1,56 +0,0 @@
meta {
name: Create Store
type: http
seq: 2
}
post {
url: {{baseUrl}}/stores
body: json
}
headers {
Content-Type: application/json
Authorization: Bearer {{authToken}}
}
body:json {
{
"store": {
"name": "Test Store Location",
"latitude": 40.7128,
"longitude": -74.006,
"chain_id": {{chainId}}
}
}
}
assert {
res.status: equals 201
}
script:post-response {
const body = res.getBody();
if (body && body.id) {
bru.setVar("storeId", body.id);
}
}
tests {
test("Create store returns store object directly", function() {
const data = res.getBody();
expect(data).to.have.property("id");
expect(data).to.have.property("name");
expect(data).to.have.property("latitude");
expect(data).to.have.property("longitude");
expect(data).to.have.property("chain_id");
expect(data).to.have.property("created_at");
expect(data).to.have.property("updated_at");
});
test("Created store has correct values", function() {
const data = res.getBody();
expect(data.name).to.equal("Test Store Location");
expect(data.chain_id).to.equal(Number(bru.getVar("chainId")));
});
}

View file

@ -1,23 +0,0 @@
meta {
name: Delete Store
type: http
seq: 5
}
delete {
url: {{baseUrl}}/stores/{{storeId}}
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 204
}
tests {
test("Delete store returns 204 no content", function() {
expect(res.getStatus()).to.equal(204);
});
}

View file

@ -1,35 +0,0 @@
meta {
name: Get Store
type: http
seq: 3
}
get {
url: {{baseUrl}}/stores/{{storeId}}
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 200
}
tests {
test("Get store returns store object directly", function() {
const data = res.getBody();
expect(data).to.have.property("id");
expect(data).to.have.property("name");
expect(data).to.have.property("latitude");
expect(data).to.have.property("longitude");
expect(data).to.have.property("chain_id");
expect(data).to.have.property("created_at");
expect(data).to.have.property("updated_at");
});
test("Store id matches requested id", function() {
const data = res.getBody();
expect(data.id).to.equal(Number(bru.getVar("storeId")));
});
}

View file

@ -1,35 +0,0 @@
meta {
name: List Stores
type: http
seq: 1
}
get {
url: {{baseUrl}}/stores
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 200
}
tests {
test("List stores returns an array", function() {
const data = res.getBody();
expect(data).to.be.an("array");
});
test("Each store has correct shape", function() {
const data = res.getBody();
data.forEach(function(store) {
expect(store).to.have.property("id");
expect(store).to.have.property("name");
expect(store).to.have.property("latitude");
expect(store).to.have.property("longitude");
expect(store).to.have.property("chain_id");
});
});
}

View file

@ -1,36 +0,0 @@
meta {
name: Update Store
type: http
seq: 4
}
patch {
url: {{baseUrl}}/stores/{{storeId}}
body: json
}
headers {
Content-Type: application/json
Authorization: Bearer {{authToken}}
}
body:json {
{
"store": {
"name": "Updated Store Name"
}
}
}
assert {
res.status: equals 200
}
tests {
test("Update store returns updated object", function() {
const data = res.getBody();
expect(data).to.have.property("id");
expect(data).to.have.property("name");
expect(data.name).to.equal("Updated Store Name");
});
}

View file

@ -1,26 +0,0 @@
meta {
name: Delete User
type: http
seq: 2
}
delete {
url: {{baseUrl}}/users/{{userId}}
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 200
res.body.success: equals true
}
tests {
test("Delete user returns { success: true }", function() {
const data = res.getBody();
expect(data).to.have.property("success");
expect(data.success).to.equal(true);
});
}

View file

@ -1,34 +0,0 @@
meta {
name: Get User
type: http
seq: 1
}
get {
url: {{baseUrl}}/users/{{userId}}
}
headers {
Authorization: Bearer {{authToken}}
}
assert {
res.status: equals 200
res.body.success: equals true
}
tests {
test("Get user returns wrapped success response", function() {
const data = res.getBody();
expect(data).to.have.property("success");
expect(data.success).to.equal(true);
expect(data).to.have.property("data");
});
test("User data has correct shape", function() {
const data = res.getBody();
expect(data.data).to.have.property("id");
expect(data.data).to.have.property("email");
expect(data.data).to.have.property("name");
});
}

View file

@ -6,11 +6,6 @@ Rails.application.routes.draw do
resources :products do resources :products do
resources :price_reports, only: [:index, :create] resources :price_reports, only: [:index, :create]
end end
post 'login', to: 'sessions#create'
delete 'logout', to: 'sessions#destroy'
post 'signup', to: 'users#create'
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.

View file

@ -1,13 +0,0 @@
class CreateSessions < ActiveRecord::Migration[8.0]
def change
create_table :sessions do |t|
t.references :user, null: false, foreign_key: true
t.string :token
t.string :user_agent
t.string :ip_address
t.timestamps
end
add_index :sessions, :token, unique: true
end
end

View file

@ -1,5 +0,0 @@
class RemoveTokenFromUsers < ActiveRecord::Migration[8.0]
def change
remove_column :users, :token, :string
end
end

14
db/schema.rb generated
View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_06_09_155729) do ActiveRecord::Schema[8.0].define(version: 2026_05_30_204821) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql" enable_extension "pg_catalog.plpgsql"
@ -81,17 +81,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_09_155729) do
t.index ["barcode"], name: "index_products_on_barcode", unique: true t.index ["barcode"], name: "index_products_on_barcode", unique: true
end end
create_table "sessions", force: :cascade do |t|
t.bigint "user_id", null: false
t.string "token"
t.string "user_agent"
t.string "ip_address"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["token"], name: "index_sessions_on_token", unique: true
t.index ["user_id"], name: "index_sessions_on_user_id"
end
create_table "stores", force: :cascade do |t| create_table "stores", force: :cascade do |t|
t.string "name" t.string "name"
t.float "latitude" t.float "latitude"
@ -129,7 +118,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_09_155729) do
add_foreign_key "price_reports", "products" add_foreign_key "price_reports", "products"
add_foreign_key "price_reports", "stores" add_foreign_key "price_reports", "stores"
add_foreign_key "price_reports", "users" add_foreign_key "price_reports", "users"
add_foreign_key "sessions", "users"
add_foreign_key "stores", "chains" add_foreign_key "stores", "chains"
add_foreign_key "votes", "users" add_foreign_key "votes", "users"
end end

View file

@ -1,3 +0,0 @@
class ApplicationController
@current_user: User
end

View file

@ -1,7 +0,0 @@
require "test_helper"
class SessionsControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require "test_helper"
class UsersControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

View file

@ -1,34 +0,0 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
# == Schema Information
#
# Table name: sessions
#
# id :bigint not null, primary key
# ip_address :string
# token :string
# user_agent :string
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
#
# Indexes
#
# index_sessions_on_token (token) UNIQUE
# index_sessions_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
#
one:
user: one
token: MyString
user_agent: MyString
ip_address: MyString
two:
user: two
token: MyString
user_agent: MyString
ip_address: MyString

View file

@ -1,28 +0,0 @@
# == Schema Information
#
# Table name: sessions
#
# id :bigint not null, primary key
# ip_address :string
# token :string
# user_agent :string
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
#
# Indexes
#
# index_sessions_on_token (token) UNIQUE
# index_sessions_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
#
require "test_helper"
class SessionTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end