Skip to content

Commit 08f3674

Browse files
committed
Final: App Working
1 parent c9ccec6 commit 08f3674

File tree

16 files changed

+450
-0
lines changed

16 files changed

+450
-0
lines changed

.idea/google-java-format.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bootstrap-tasknexus-ruby.sh

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
# 1) Clean and create ruby/ structure
5+
rm -rf ruby
6+
mkdir -p ruby/{app/models,app/controllers,config,initializers,public}
7+
8+
# 2) Gemfile
9+
cat > ruby/Gemfile << 'EOF'
10+
source 'https://rubygems.org'
11+
ruby '3.1.0'
12+
13+
gem 'sinatra', require: 'sinatra/base'
14+
gem 'mongoid', '~> 7.3'
15+
gem 'jwt'
16+
gem 'bcrypt'
17+
gem 'rack-cors'
18+
gem 'json'
19+
EOF
20+
21+
# 3) config/mongoid.yml
22+
cat > ruby/config/mongoid.yml << 'EOF'
23+
development:
24+
clients:
25+
default:
26+
uri: <%= ENV['MONGODB_URI'] || 'mongodb://127.0.0.1:27017/tasknexus_dev' %>
27+
production:
28+
clients:
29+
default:
30+
uri: <%= ENV['MONGODB_URI'] %>
31+
EOF
32+
33+
# 4) initializers/jwt.rb
34+
cat > ruby/initializers/jwt.rb << 'EOF'
35+
JWT_SECRET = ENV['JWT_SECRET'] || 'super$ecretKey' # change in prod!
36+
def issue_token(payload)
37+
JWT.encode(payload, JWT_SECRET, 'HS256')
38+
end
39+
def decode_token(token)
40+
JWT.decode(token, JWT_SECRET, true, algorithm: 'HS256')[0]
41+
rescue
42+
nil
43+
end
44+
EOF
45+
46+
# 5) app/models/user.rb
47+
cat > ruby/app/models/user.rb << 'EOF'
48+
require 'mongoid'
49+
require 'bcrypt'
50+
51+
class User
52+
include Mongoid::Document
53+
include Mongoid::Timestamps
54+
field :username, type: String
55+
field :password_hash, type: String
56+
57+
validates :username, presence: true, uniqueness: true
58+
validates :password_hash, presence: true
59+
60+
def password=(raw)
61+
self.password_hash = BCrypt::Password.create(raw)
62+
end
63+
64+
def authenticate(raw)
65+
BCrypt::Password.new(password_hash) == raw
66+
end
67+
end
68+
EOF
69+
70+
# 6) app/models/task.rb
71+
cat > ruby/app/models/task.rb << 'EOF'
72+
require 'mongoid'
73+
74+
class Task
75+
include Mongoid::Document
76+
include Mongoid::Timestamps
77+
field :title, type: String
78+
field :completed, type: Mongoid::Boolean, default: false
79+
field :position, type: Integer
80+
81+
belongs_to :user
82+
83+
validates :title, presence: true
84+
end
85+
EOF
86+
87+
# 7) app/controllers/auth_controller.rb
88+
cat > ruby/app/controllers/auth_controller.rb << 'EOF'
89+
require 'sinatra/base'
90+
require 'json'
91+
require_relative '../models/user'
92+
require_relative '../../initializers/jwt'
93+
94+
class AuthController < Sinatra::Base
95+
post '/register' do
96+
data = JSON.parse(request.body.read)
97+
user = User.new(username: data['username'])
98+
user.password = data['password']
99+
halt 400, { error: user.errors.full_messages }.to_json unless user.save
100+
token = issue_token({ user_id: user.id.to_s })
101+
{ token: token }.to_json
102+
end
103+
104+
post '/login' do
105+
data = JSON.parse(request.body.read)
106+
user = User.find_by(username: data['username']) rescue nil
107+
halt 401, { error: 'Invalid credentials' }.to_json unless user&.authenticate(data['password'])
108+
{ token: issue_token({ user_id: user.id.to_s }) }.to_json
109+
end
110+
end
111+
EOF
112+
113+
# 8) app/controllers/tasks_controller.rb
114+
cat > ruby/app/controllers/tasks_controller.rb << 'EOF'
115+
require 'sinatra/base'
116+
require 'json'
117+
require_relative '../models/task'
118+
require_relative '../models/user'
119+
require_relative '../../initializers/jwt'
120+
121+
class TasksController < Sinatra::Base
122+
before do
123+
pass if request.path_info =~ %r{^/tasks/public}
124+
auth_header = request.env['HTTP_AUTHORIZATION'] || ''
125+
token = auth_header.split(' ').last
126+
payload = decode_token(token)
127+
halt 401, { error: 'Unauthorized' }.to_json unless payload
128+
@current_user = User.find(payload['user_id']) rescue nil
129+
halt 401, { error: 'Unauthorized' }.to_json unless @current_user
130+
end
131+
132+
# public endpoint for stats
133+
get '/tasks/public/stats' do
134+
total = Task.count
135+
completed = Task.where(completed: true).count
136+
{ total: total, completed: completed, incomplete: total - completed }.to_json
137+
end
138+
139+
# CRUD
140+
get '/tasks' do
141+
tasks = @current_user.tasks.order_by(:position.asc)
142+
tasks.to_json
143+
end
144+
145+
post '/tasks' do
146+
data = JSON.parse(request.body.read)
147+
task = @current_user.tasks.new(title: data['title'], position: data['position'])
148+
halt 400, { error: task.errors.full_messages }.to_json unless task.save
149+
status 201
150+
task.to_json
151+
end
152+
153+
put '/tasks/:id' do
154+
task = @current_user.tasks.find(params['id'])
155+
data = JSON.parse(request.body.read)
156+
task.update(title: data['title'], completed: data['completed'], position: data['position'])
157+
task.to_json
158+
end
159+
160+
delete '/tasks/:id' do
161+
task = @current_user.tasks.find(params['id'])
162+
task.destroy
163+
status 204
164+
end
165+
end
166+
EOF
167+
168+
# 9) app.rb (main entry)
169+
cat > ruby/app.rb << 'EOF'
170+
require 'sinatra/base'
171+
require 'mongoid'
172+
require 'rack/cors'
173+
require_relative './config/mongoid'
174+
require_relative './initializers/jwt'
175+
require_relative 'app/controllers/auth_controller'
176+
require_relative 'app/controllers/tasks_controller'
177+
178+
Mongoid.load!('config/mongoid.yml', ENV['RACK_ENV'] || :development)
179+
180+
class TaskNexusApp < Sinatra::Base
181+
use Rack::Cors do
182+
allow do
183+
origins '*'
184+
resource '*', headers: :any, methods: [:get, :post, :put, :delete, :options]
185+
end
186+
end
187+
188+
use AuthController
189+
use TasksController
190+
191+
get '/' do
192+
'TaskNexus API is running!'
193+
end
194+
end
195+
196+
run TaskNexusApp
197+
EOF
198+
199+
# 10) config.ru for Rack
200+
cat > ruby/config.ru << 'EOF'
201+
require './app'
202+
run TaskNexusApp
203+
EOF
204+
205+
# 11) Dockerfile
206+
cat > ruby/Dockerfile << 'EOF'
207+
FROM ruby:3.1
208+
209+
WORKDIR /usr/src/app
210+
COPY Gemfile* ./
211+
RUN bundle install
212+
213+
COPY . .
214+
EXPOSE 4567
215+
CMD ["bundle", "exec", "rackup", "--host", "0.0.0.0", "-p", "4567"]
216+
EOF
217+
218+
# 12) .env.example
219+
cat > ruby/.env.example << 'EOF'
220+
MONGODB_URI=mongodb://mongo:27017/tasknexus_prod
221+
JWT_SECRET=replace_with_strong_secret
222+
EOF
223+
224+
echo "✅ Ruby backend scaffold complete under ruby/"
225+
echo "• To run locally without Docker:"
226+
echo " cd ruby"
227+
echo " export MONGODB_URI=mongodb://127.0.0.1:27017/tasknexus_dev"
228+
echo " bundle install"
229+
echo " bundle exec rackup"
230+
echo "• To run via Docker:"
231+
echo " cd ruby"
232+
echo " docker build -t hoangsonww/tasknexus-api ."
233+
echo " docker run -e MONGODB_URI=mongodb://host.docker.internal:27017/tasknexus_dev -e JWT_SECRET=you_choose --rm -p 4567:4567 hoangsonww/tasknexus-api"

ruby/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
MONGODB_URI=mongodb://mongo:27017/tasknexus_prod
2+
JWT_SECRET=replace_with_strong_secret

ruby/Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM ruby:3.1
2+
3+
WORKDIR /usr/src/app
4+
COPY Gemfile* ./
5+
RUN bundle install
6+
7+
COPY . .
8+
EXPOSE 4567
9+
CMD ["bundle", "exec", "rackup", "--host", "0.0.0.0", "-p", "4567"]

ruby/Gemfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
source 'https://rubygems.org'
2+
ruby '3.1.0'
3+
4+
gem 'sinatra', require: 'sinatra/base'
5+
gem 'mongoid', '~> 7.3'
6+
gem 'jwt'
7+
gem 'bcrypt'
8+
gem 'rack-cors'
9+
gem 'json'

ruby/app.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
require 'sinatra/base'
2+
require 'mongoid'
3+
require 'rack/cors'
4+
require_relative './config/mongoid'
5+
require_relative './initializers/jwt'
6+
require_relative 'app/controllers/auth_controller'
7+
require_relative 'app/controllers/tasks_controller'
8+
9+
Mongoid.load!('config/mongoid.yml', ENV['RACK_ENV'] || :development)
10+
11+
class TaskNexusApp < Sinatra::Base
12+
use Rack::Cors do
13+
allow do
14+
origins '*'
15+
resource '*', headers: :any, methods: [:get, :post, :put, :delete, :options]
16+
end
17+
end
18+
19+
use AuthController
20+
use TasksController
21+
22+
get '/' do
23+
'TaskNexus API is running!'
24+
end
25+
end
26+
27+
run TaskNexusApp
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
require 'sinatra/base'
2+
require 'json'
3+
require_relative '../models/user'
4+
require_relative '../../initializers/jwt'
5+
6+
class AuthController < Sinatra::Base
7+
post '/register' do
8+
data = JSON.parse(request.body.read)
9+
user = User.new(username: data['username'])
10+
user.password = data['password']
11+
halt 400, { error: user.errors.full_messages }.to_json unless user.save
12+
token = issue_token({ user_id: user.id.to_s })
13+
{ token: token }.to_json
14+
end
15+
16+
post '/login' do
17+
data = JSON.parse(request.body.read)
18+
user = User.find_by(username: data['username']) rescue nil
19+
halt 401, { error: 'Invalid credentials' }.to_json unless user&.authenticate(data['password'])
20+
{ token: issue_token({ user_id: user.id.to_s }) }.to_json
21+
end
22+
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
require 'sinatra/base'
2+
require 'json'
3+
require_relative '../models/task'
4+
require_relative '../models/user'
5+
require_relative '../../initializers/jwt'
6+
7+
class TasksController < Sinatra::Base
8+
before do
9+
pass if request.path_info =~ %r{^/tasks/public}
10+
auth_header = request.env['HTTP_AUTHORIZATION'] || ''
11+
token = auth_header.split(' ').last
12+
payload = decode_token(token)
13+
halt 401, { error: 'Unauthorized' }.to_json unless payload
14+
@current_user = User.find(payload['user_id']) rescue nil
15+
halt 401, { error: 'Unauthorized' }.to_json unless @current_user
16+
end
17+
18+
# public endpoint for stats
19+
get '/tasks/public/stats' do
20+
total = Task.count
21+
completed = Task.where(completed: true).count
22+
{ total: total, completed: completed, incomplete: total - completed }.to_json
23+
end
24+
25+
# CRUD
26+
get '/tasks' do
27+
tasks = @current_user.tasks.order_by(:position.asc)
28+
tasks.to_json
29+
end
30+
31+
post '/tasks' do
32+
data = JSON.parse(request.body.read)
33+
task = @current_user.tasks.new(title: data['title'], position: data['position'])
34+
halt 400, { error: task.errors.full_messages }.to_json unless task.save
35+
status 201
36+
task.to_json
37+
end
38+
39+
put '/tasks/:id' do
40+
task = @current_user.tasks.find(params['id'])
41+
data = JSON.parse(request.body.read)
42+
task.update(title: data['title'], completed: data['completed'], position: data['position'])
43+
task.to_json
44+
end
45+
46+
delete '/tasks/:id' do
47+
task = @current_user.tasks.find(params['id'])
48+
task.destroy
49+
status 204
50+
end
51+
end

ruby/app/models/task.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
require 'mongoid'
2+
3+
class Task
4+
include Mongoid::Document
5+
include Mongoid::Timestamps
6+
field :title, type: String
7+
field :completed, type: Mongoid::Boolean, default: false
8+
field :position, type: Integer
9+
10+
belongs_to :user
11+
12+
validates :title, presence: true
13+
end

0 commit comments

Comments
 (0)