From 3665af104126c15f1fbd783a4c1b04d89d9a8c77 Mon Sep 17 00:00:00 2001 From: 24c02 <163450896+24c02@users.noreply.github.com> Date: Sat, 6 Dec 2025 00:55:29 -0500 Subject: [PATCH] first pass at backend! --- backend/.gitignore | 1 + backend/Gemfile | 23 ++++ backend/Gemfile.lock | 196 +++++++++++++++++++++++++++ backend/api/app.rb | 8 ++ backend/api/auth.rb | 32 +++++ backend/api/base.rb | 6 + backend/api/session_helpers.rb | 15 ++ backend/api/stickers.rb | 25 ++++ backend/boot.rb | 25 ++++ backend/config.ru | 38 ++++++ backend/models/application_record.rb | 3 + backend/models/sticker.rb | 17 +++ 12 files changed, 389 insertions(+) create mode 100644 backend/.gitignore create mode 100644 backend/Gemfile create mode 100644 backend/Gemfile.lock create mode 100644 backend/api/app.rb create mode 100644 backend/api/auth.rb create mode 100644 backend/api/base.rb create mode 100644 backend/api/session_helpers.rb create mode 100644 backend/api/stickers.rb create mode 100644 backend/boot.rb create mode 100644 backend/config.ru create mode 100644 backend/models/application_record.rb create mode 100644 backend/models/sticker.rb diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/backend/Gemfile b/backend/Gemfile new file mode 100644 index 0000000..40b8c5e --- /dev/null +++ b/backend/Gemfile @@ -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' diff --git a/backend/Gemfile.lock b/backend/Gemfile.lock new file mode 100644 index 0000000..fc33ae1 --- /dev/null +++ b/backend/Gemfile.lock @@ -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 diff --git a/backend/api/app.rb b/backend/api/app.rb new file mode 100644 index 0000000..696b505 --- /dev/null +++ b/backend/api/app.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class App < Grape::API + format :json + + mount Auth + mount Stickers +end diff --git a/backend/api/auth.rb b/backend/api/auth.rb new file mode 100644 index 0000000..b406472 --- /dev/null +++ b/backend/api/auth.rb @@ -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 diff --git a/backend/api/base.rb b/backend/api/base.rb new file mode 100644 index 0000000..4c9c36f --- /dev/null +++ b/backend/api/base.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Base < Grape::API + format :json + helpers SessionHelpers +end diff --git a/backend/api/session_helpers.rb b/backend/api/session_helpers.rb new file mode 100644 index 0000000..7022980 --- /dev/null +++ b/backend/api/session_helpers.rb @@ -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 diff --git a/backend/api/stickers.rb b/backend/api/stickers.rb new file mode 100644 index 0000000..c02b19f --- /dev/null +++ b/backend/api/stickers.rb @@ -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 diff --git a/backend/boot.rb b/backend/boot.rb new file mode 100644 index 0000000..e5429f6 --- /dev/null +++ b/backend/boot.rb @@ -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 diff --git a/backend/config.ru b/backend/config.ru new file mode 100644 index 0000000..3878ebd --- /dev/null +++ b/backend/config.ru @@ -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) +} diff --git a/backend/models/application_record.rb b/backend/models/application_record.rb new file mode 100644 index 0000000..c8efe69 --- /dev/null +++ b/backend/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < AirctiveRecord::Base + self.base_key = ENV['AIRTABLE_BASE'] +end diff --git a/backend/models/sticker.rb b/backend/models/sticker.rb new file mode 100644 index 0000000..0d19dc2 --- /dev/null +++ b/backend/models/sticker.rb @@ -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 \ No newline at end of file