Merge pull request #2 from hackclub/coolify-fix

really meant to do that on main
This commit is contained in:
Euan R 2026-01-21 21:33:23 +01:00 committed by GitHub
commit 35e0809ce7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 200 additions and 122 deletions

View file

@ -1,4 +1,7 @@
# =============================================================================
# BACKEND (Ruby/Puma)
# =============================================================================
FROM ruby:3.3-alpine AS backend
RUN apk add --no-cache build-base tzdata
WORKDIR /app
@ -10,7 +13,9 @@ COPY backend/ .
EXPOSE 9292
CMD ["bundle", "exec", "puma", "-p", "9292", "-e", "production"]
# frontend
# =============================================================================
# FRONTEND (SvelteKit/Node)
# =============================================================================
FROM node:22-alpine AS frontend
WORKDIR /app

View file

@ -1,7 +1,8 @@
.bundle
vendor/bundle
.env
.env.*
!.env.example
.git
*.log
.vendor

View file

@ -11,12 +11,7 @@ class Auth < Grape::API
get 'oidc/callback' do
auth = env['omniauth.auth']
session[:user] = {
id: auth.uid,
email: auth.info.email,
name: auth.info.name,
slack_id: auth.info.slack_id
}
session[:user] = User.from_omniauth(auth).to_h
redirect ENV.fetch('AUTH_SUCCESS_REDIRECT', '/')
end
@ -27,7 +22,7 @@ class Auth < Grape::API
get :me do
error!('Unauthorized', 401) unless current_user
current_user
current_user.to_h
end
end
end

View file

@ -1,81 +1,34 @@
# frozen_string_literal: true
class StickersTable < AirctiveRecord::Base
self.base_key = ENV['AIRTABLE_BASE_ID']
self.table_name = "stickerDB"
end
class ShopTable < AirctiveRecord::Base
self.base_key = ENV['AIRTABLE_BASE_ID']
self.table_name = "shop"
end
class DesignsTable < AirctiveRecord::Base
self.base_key = ENV['AIRTABLE_BASE_ID']
self.table_name = "designs"
end
class Designs < Base
DESIGN_ALLOWED_FIELDS = %w[Name Description Image_URL CDN_URL].freeze
class Designs < Grape::API
format :json
helpers SessionHelpers
resource :designs do
get :all do
error!('Unauthorized', 401) unless current_user
user_id = current_user[:slack_id] || current_user[:id]
records = DesignsTable.all
approved = records.select { |r| r['Status']&.downcase == 'approved' }
approved.map do |record|
voted_by = (record['voted_by'] || '').split(',').map(&:strip)
{
id: record.id,
cdn_url: record['CDN_URL'],
name: record['Name'],
votes: record['Votes'] || 0,
voted: voted_by.include?(user_id)
}
end
user_id = current_user.identifier
DesignsTable.all.map { |d| d.as_approved_json(user_id) }
end
get do
error!('Unauthorized', 401) unless current_user
slack_id = current_user[:slack_id] || current_user[:id]
records = DesignsTable.all
records.select { |r| r['Slack_ID'] == slack_id }.map do |record|
{
id: record.id,
cdn_url: record['CDN_URL'],
slack_id: record['Slack_ID'],
name: record['Name'],
status: record['Status'],
votes: record['Votes'],
submitted_at: record['Created']
}
end
user_id = current_user.identifier
Design.by_user(user_id).all.map { |d| d.as_approved_json(user_id) }
end
post do
error!('Unauthorized', 401) unless current_user
puts "=== DEBUG: POST /designs ==="
puts "params: #{params.inspect}"
puts "params[:fields]: #{params[:fields].inspect}"
fields = (params[:fields] || {}).transform_keys(&:to_s)
puts "fields after transform: #{fields.inspect}"
safe_fields = fields.slice(*DESIGN_ALLOWED_FIELDS)
puts "safe_fields after slice: #{safe_fields.inspect}"
safe_fields['Slack_ID'] = current_user[:slack_id] || current_user[:id]
safe_fields['Status'] = 'Pending'
safe_fields = (params[:fields] || {}).slice(*Design::ALLOWED_FIELDS)
safe_fields['slack_id'] = current_user.identifier
safe_fields['Votes'] = 0
puts "final safe_fields: #{safe_fields.inspect}"
begin
result = DesignsTable.create(safe_fields)
puts "create result: #{result.inspect}"
result
rescue => e
puts "ERROR: #{e.class}: #{e.message}"
puts e.backtrace.first(10).join("\n")
raise
end
safe_fields['Status'] = 'pending'
Design.create(safe_fields)
end
route_param :id do
post :vote do

View file

@ -6,7 +6,8 @@ module SessionHelpers
end
def current_user
session[:user]
return nil unless session[:user]
@current_user ||= User.new(session[:user])
end
def authenticate!

View file

@ -1,34 +1,19 @@
# frozen_string_literal: true
class Shop < Base
class Shop < Grape::API
format :json
helpers SessionHelpers
resource :shop do
get do
records = ShopTable.all
records.map do |record|
{
id: record.id,
name: record["Name"],
image: record["CDN_URL"],
price: record["Cost"],
description: record["Description"]
}
end
ShopRecord.all.map(&:as_json)
end
route_param :id, type: String do
get do
record = ShopTable.find(params[:id])
record = shopRecord.find(params[:id])
error!('not found', 404) unless record
{
id: record.id,
name: record["Name"],
image: record["CDN_URL"],
price: record["cost"],
description: record["Description"]
}
end
record.as_json
end
end
end

View file

@ -1,41 +1,21 @@
# frozen_string_literal: true
class Stickers < Base
class Stickers < Grape::API
format :json
helpers SessionHelpers
resource :stickers do
get do
user_id = current_user ? (current_user[:slack_id] || current_user[:id]) : nil
records = StickersTable.all
records.map do |record|
owned_by = record["owned_by"] || ""
owners = owned_by.split(',').map(&:strip)
{
id: record.id,
name: record["Sticker Name"],
image: record["CDN_URL"],
artist: record["Artist"],
event: record["Event"],
event_URL: record["event_URL"],
owned_by: owned_by,
owned: user_id && owners.include?(user_id)
}
end
user_id = current_user&.identifier
StickerRecord.all.map { |r| r.as_json(user_id: user_id) }
end
route_param :id, type: String do
before { authenticate! }
get do
record = StickersTable.find(params[:id])
record = StickersRecord.find(params[:id])
error!('not found', 404) unless record
{
id: record.id,
name: record["Sticker Name"],
image: record["CDN_URL"],
artist: record["Artist"],
event: record["Event"],
event_URL: record["event_URL"]
}
record.as_detail_json
end
end
end

71
backend/models/design.rb Normal file
View file

@ -0,0 +1,71 @@
# frozen_string_literal: true
class Design < AirctiveRecord::Base
self.base_key = ENV['AIRTABLE_BASE_ID']
self.table_name = 'designs'
field :name, 'Name'
field :description, 'Description'
field :image_url, 'Image_URL'
field :cdn_url, 'CDN_URL'
field :slack_id, 'slack_id'
field :status, 'Status'
field :votes, 'Votes'
field :voted_by, 'voted_by'
field :created, 'Created'
ALLOWED_FIELDS = %w[Name Description Image_URL].freeze
scope :approved, -> { where(status: 'approved') }
scope :by_user, ->(slack_id) { where(slack_id: slack_id) }
def vote!(user_id)
voted_users = voted_list
if voted_users.include?(user_id)
voted_users.delete(user_id)
self.votes = [votes.to_i - 1, 0].max
else
voted_users << user_id
self.votes = votes.to_i + 1
end
self.voted_by = voted_users.join(',')
save
end
def voted_by_user?(user_id)
voted_list.include?(user_id)
end
def as_json(options = nil)
user_id = options&.dig(:user_id)
{
id: id,
cdn_url: cdn_url,
slack_id: slack_id,
name: name,
status: status,
votes: votes || 0,
submitted_at: created,
voted: user_id ? voted_by_user?(user_id) : false
}
end
def as_approved_json(options = nil)
user_id = options&.dig(:user_id)
{
id: id,
cdn_url: cdn_url,
name: name,
votes: votes || 0,
voted: user_id ? voted_by_user?(user_id) : false
}
end
private
def voted_list
(voted_by || '').split(',').map(&:strip).reject(&:empty?)
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class ShopRecord < AirctiveRecord::Base
self.base_key = ENV['AIRTABLE_BASE_ID']
self.table_name = 'shop'
field :name, 'Name'
field :image, 'CDN_URL'
field :price, 'Cost'
field :description, 'Description'
def as_json(options = nil)
{
id: id,
name: name,
image: image,
price: price,
description: description
}
end
end

View file

@ -0,0 +1,36 @@
frozen_string_literal: true
class StickerRecord < AirctiveRecord::Base
self.base_key = ENV['AIRTABLE_BASE_ID']
self.table_name = 'stickerDB'
field :name, 'Sticker Name'
field :image, 'CDN_URL'
field :artist, 'Artist'
field :event, 'Event'
field :owned_by, 'owned_by'
def as_json(options = nil)
user_id = options&.dig(:user_id)
owners = (owned_by || '').split(',').map(&:strip)
{
id: id,
name: name,
image: image,
artist: artist,
event: event,
owned_by: owned_by,
owned: user_id && owners.include?(user_id)
}
end
def as_detail_json(options = nil)
{
id: id,
name: name,
image: image,
artist: artist,
event: event
}
end
end

29
backend/models/user.rb Normal file
View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
class User
attr_accessor :id, :email, :name, :slack_id
def initialize(attrs = {})
@id = attrs[:id]
@email = attrs[:email]
@name = attrs[:name]
@slack_id = attrs[:slack_id]
end
def self.from_omniauth(auth)
new(
id: auth.uid,
email: auth.info.email,
name: auth.info.name,
slack_id: auth.info.slack_id
)
end
def identifier
slack_id || id
end
def to_h
{ id: id, email: email, name: name, slack_id: slack_id }
end
end

View file

@ -10,7 +10,7 @@ services:
- path: ./backend/.env
required: false
environment:
FRONTEND_URL: ${FRONTEND_URL:-http://localhost:5173}
FRONTEND_URL: ${FRONTEND_URL:-https://stickers.hackclub.com}
frontend:
build:
@ -20,7 +20,7 @@ services:
expose:
- "3000"
environment:
ORIGIN: ${ORIGIN:-http://localhost:5173}
ORIGIN: ${ORIGIN:-https://stickers.hackclub.com}
BACKEND_URL: http://backend:9292
depends_on:
depends_on:
- backend