first pass at backend!

This commit is contained in:
24c02 2025-12-06 00:55:29 -05:00
commit 3665af1041
12 changed files with 389 additions and 0 deletions

1
backend/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.env

23
backend/Gemfile Normal file
View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
source 'https://rubygems.org'
# gem "rails"
gem 'grape'
gem 'airctiverecord', '~> 0.2.1'
gem 'dotenv', '~> 3.2'
gem 'zeitwerk', '~> 2.6'
gem 'rackup'
gem 'puma', '~> 7.1'
gem 'rack', '~> 3.2'
gem 'omniauth'
gem 'omniauth_openid_connect'
gem 'rack-session'

196
backend/Gemfile.lock Normal file
View file

@ -0,0 +1,196 @@
GEM
remote: https://rubygems.org/
specs:
activemodel (8.1.1)
activesupport (= 8.1.1)
activesupport (8.1.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
json
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
aes_key_wrap (1.1.0)
airctiverecord (0.2.1)
activemodel (>= 6.0)
activesupport (>= 6.0)
airrel (>= 0.2.0)
norairrecord (~> 0.5)
airrel (0.2.1)
norairrecord (>= 0.1.0)
attr_required (1.0.2)
base64 (0.3.0)
bigdecimal (3.3.1)
bindata (2.5.1)
concurrent-ruby (1.3.5)
connection_pool (2.5.5)
date (3.5.0)
dotenv (3.2.0)
drb (2.2.3)
dry-configurable (1.3.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-core (1.1.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
dry-inflector (1.2.0)
dry-logic (1.6.0)
bigdecimal
concurrent-ruby (~> 1.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-types (1.8.3)
bigdecimal (~> 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
email_validator (2.2.4)
activemodel
faraday (2.14.0)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-follow_redirects (0.4.0)
faraday (>= 1, < 3)
faraday-net_http (3.4.2)
net-http (~> 0.5)
faraday-net_http_persistent (2.3.1)
faraday (~> 2.5)
net-http-persistent (>= 4.0.4, < 5)
grape (3.0.1)
activesupport (>= 7.0)
dry-configurable
dry-types (>= 1.1)
mustermann-grape (~> 1.1.0)
rack (>= 2)
zeitwerk
hashie (5.0.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
json (2.17.1)
json-jwt (1.17.0)
activesupport (>= 4.2)
aes_key_wrap
base64
bindata
faraday (~> 2.0)
faraday-follow_redirects
logger (1.7.0)
mail (2.9.0)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
mini_mime (1.1.5)
minitest (5.26.2)
mustermann (3.0.4)
ruby2_keywords (~> 0.0.1)
mustermann-grape (1.1.0)
mustermann (>= 1.0.0)
net-http (0.8.0)
uri (>= 0.11.1)
net-http-persistent (4.0.6)
connection_pool (~> 2.2, >= 2.2.4)
net-imap (0.5.12)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
norairrecord (0.5.1)
faraday (>= 1.0, < 3.0)
faraday-net_http_persistent
net-http-persistent
omniauth (2.1.4)
hashie (>= 3.4.6)
logger
rack (>= 2.2.3)
rack-protection
omniauth_openid_connect (0.8.0)
omniauth (>= 1.9, < 3)
openid_connect (~> 2.2)
openid_connect (2.3.1)
activemodel
attr_required (>= 1.0.0)
email_validator
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.16)
mail
rack-oauth2 (~> 2.2)
swd (~> 2.0)
tzinfo
validate_url
webfinger (~> 2.0)
public_suffix (7.0.0)
puma (7.1.0)
nio4r (~> 2.0)
rack (3.2.4)
rack-oauth2 (2.3.0)
activesupport
attr_required
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (4.2.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rackup (2.3.1)
rack (>= 3)
ruby2_keywords (0.0.5)
securerandom (0.4.1)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
faraday (~> 2.0)
faraday-follow_redirects
timeout (0.4.4)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uri (1.1.1)
validate_url (1.0.15)
activemodel (>= 3.0.0)
public_suffix
webfinger (2.1.3)
activesupport
faraday (~> 2.0)
faraday-follow_redirects
zeitwerk (2.7.3)
PLATFORMS
arm64-darwin-24
ruby
DEPENDENCIES
airctiverecord (~> 0.2.1)
dotenv (~> 3.2)
grape
omniauth
omniauth_openid_connect
puma (~> 7.1)
rack (~> 3.2)
rack-session
rackup
zeitwerk (~> 2.6)
BUNDLED WITH
2.7.2

8
backend/api/app.rb Normal file
View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
class App < Grape::API
format :json
mount Auth
mount Stickers
end

32
backend/api/auth.rb Normal file
View file

@ -0,0 +1,32 @@
# frozen_string_literal: true
class Auth < Grape::API
format :json
helpers SessionHelpers
namespace :auth do
get :login do
redirect '/auth/oidc'
end
get 'oidc/callback' do
auth = env['omniauth.auth']
session[:user] = {
id: auth.uid,
email: auth.info.email,
name: auth.info.name
}
redirect ENV.fetch('AUTH_SUCCESS_REDIRECT', '/')
end
get :logout do
session.clear
redirect ENV.fetch('AUTH_LOGOUT_REDIRECT', '/')
end
get :me do
error!('Unauthorized', 401) unless current_user
current_user
end
end
end

6
backend/api/base.rb Normal file
View file

@ -0,0 +1,6 @@
# frozen_string_literal: true
class Base < Grape::API
format :json
helpers SessionHelpers
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
module SessionHelpers
def session
env['rack.session']
end
def current_user
session[:user]
end
def authenticate!
error!('Unauthorized', 401) unless current_user
end
end

25
backend/api/stickers.rb Normal file
View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
class Stickers < Base
helpers SessionHelpers
resource :stickers do
get do
Sticker.only_active.all.map(&:as_json) + [current_user: current_user]
end
before do
@sticker = Sticker.only_active.where(autonumber: params[:id]).first || error!('not found', 404)
end
route_param :id, type: String do
before { authenticate! }
get do
@sticker.as_json
end
get '/backwards' do
@sticker.name.reverse
end
end
end
end

25
backend/boot.rb Normal file
View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'bundler/setup'
require 'dotenv'
require 'zeitwerk'
require 'airctiverecord'
require 'grape'
Dotenv.load
Norairrecord.api_key = ENV['AIRTABLE_PAT']
loader = Zeitwerk::Loader.new
loader.push_dir("#{__dir__}/models")
loader.push_dir("#{__dir__}/api")
loader.enable_reloading
loader.setup
loader.eager_load
def reload!
loader.reload
end
LOADER = loader

38
backend/config.ru Normal file
View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'dotenv'
Dotenv.load
require_relative 'boot'
require 'grape'
require 'rack/session'
require 'omniauth'
require 'omniauth_openid_connect'
use Rack::Session::Cookie,
key: 'stickers.session',
secret: ENV.fetch('SESSION_SECRET'),
same_site: :lax,
expire_after: 86_400 * 7
OmniAuth.config.allowed_request_methods = %i[get post]
use OmniAuth::Builder do
provider :openid_connect,
name: :oidc,
issuer: ENV.fetch('OIDC_ISSUER'),
discovery: true,
client_options: {
identifier: ENV.fetch('OIDC_CLIENT_ID'),
secret: ENV.fetch('OIDC_CLIENT_SECRET'),
redirect_uri: ENV.fetch('OIDC_REDIRECT_URI'),
host: 'auth.hackclub.com',
scheme: 'https'
},
scope: %i[openid email profile name address]
end
run lambda { |env|
LOADER.reload
App.call(env)
}

View file

@ -0,0 +1,3 @@
class ApplicationRecord < AirctiveRecord::Base
self.base_key = ENV['AIRTABLE_BASE']
end

17
backend/models/sticker.rb Normal file
View file

@ -0,0 +1,17 @@
class Sticker < ApplicationRecord
self.table_name = "tbl9kLyUrZNCJWf3L"
field :name, "Name"
field :active, "active"
field :autonumber, "id"
field :image_attachment, "image"
scope :only_active, -> { where(active: true) }
def as_json(options = nil)
{ id: autonumber, name: name, image: }
end
def image = image_attachment&.dig(0, "url")
end