mirror of
https://github.com/TracksApp/tracks.git
synced 2025-12-17 15:50:13 +01:00
This changeset introduces some integrated web service type features that take advantage
of the Rails 1.1 responds_to functionality. It also lays a foundation for future API enhancements. Basically, if you request the /projects, contexts/ or notes/ URLs with a client that specifies that it wants XML, Tracks will return XML. See DHH on the Accept header (http://www.loudthinking.com/arc/000572.html). But there's a wrinkle. The controller actions mapped to these URLs are protected by an authentication filter. In normal use, Tracks redirects an unauthenticated user to the login screen for session-based authentication. I've added a secondary authentication check that looks for a valid username and password coming from HTTP_BASIC authentication. To test out the new functionality, try this: curl -H 'Accept: application/xml' --basic --user YOUR_TRACKS_USERNAME:YOUR_TRACKS_PASSWORD http://localhost:3000/projects/ curl -H 'Accept: application/xml' --basic --user YOUR_TRACKS_USERNAME:YOUR_TRACKS_PASSWORD http://localhost:3000/contexts/ curl -H 'Accept: application/xml' --basic --user YOUR_TRACKS_USERNAME:YOUR_TRACKS_PASSWORD http://localhost:3000/notes/ HTTP_BASIC sends passwords in plain text, so the use of https is encouraged. I haven't tested this on a shared host yet, but Coda Hale, whose simple_http_auth inspired this solution and provided some copy and paste code for it (thanks, Coda!), has some notes about how to make it work in his plugin readme (http://svn.codahale.com/simple_http_auth/README). To wit, putting the following in .htaccess: RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] My thinking on this architecture is as follows: 1) Follow the spirit of responds_to and DRY to leverage existing controller code for API functionality 2) Get away from using the user token for API interactions. Let's keep it for feeds, so it's basically a "lite" form of security for read-only feeds. 3) Keep Tracks in shape to adopt the simply_restful plugin being developed alongside Rails Edge There's no real new functionality in this release that the existing API didn't provide (except for seeing your notes as XML, and somehow I don't think people are clamoring for that), but this work is an important step to being able to implement the types of API features people have been asking for. While I was at it, I did some refactoring to the login_controller for readability and style. Finally, I replaced the activity indicator graphic to work with the new navigation background color. git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@251 a4c988fc-2ded-0310-b66e-134b36920a42
This commit is contained in:
parent
3da6fe2525
commit
463a61f514
7 changed files with 93 additions and 43 deletions
|
|
@ -2,7 +2,7 @@ class ContextController < ApplicationController
|
||||||
|
|
||||||
helper :todo
|
helper :todo
|
||||||
|
|
||||||
before_filter :login_required
|
prepend_before_filter :login_required
|
||||||
layout "standard"
|
layout "standard"
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
@ -16,6 +16,10 @@ class ContextController < ApplicationController
|
||||||
def list
|
def list
|
||||||
self.init
|
self.init
|
||||||
@page_title = "TRACKS::List Contexts"
|
@page_title = "TRACKS::List Contexts"
|
||||||
|
respond_to do |wants|
|
||||||
|
wants.html
|
||||||
|
wants.xml { render :xml => @contexts.to_xml }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Filter the projects to show just the one passed in the URL
|
# Filter the projects to show just the one passed in the URL
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,8 @@ class LoginController < ApplicationController
|
||||||
session['user_id'] = @user.id
|
session['user_id'] = @user.id
|
||||||
# If checkbox on login page checked, we don't expire the session after 1 hour
|
# If checkbox on login page checked, we don't expire the session after 1 hour
|
||||||
# of inactivity
|
# of inactivity
|
||||||
session['noexpiry']= params['user_noexpiry']
|
session['noexpiry'] = params['user_noexpiry']
|
||||||
if session['noexpiry'] == "on"
|
msg = (should_expire_sessions?) ? "will not expire." : "will expire after 1 hour of inactivity."
|
||||||
msg = "will not expire."
|
|
||||||
else
|
|
||||||
msg = "will expire after 1 hour of inactivity."
|
|
||||||
end
|
|
||||||
flash['notice'] = "Login successful: session #{msg}"
|
flash['notice'] = "Login successful: session #{msg}"
|
||||||
redirect_back_or_default :controller => "todo", :action => "list"
|
redirect_back_or_default :controller => "todo", :action => "list"
|
||||||
else
|
else
|
||||||
|
|
@ -27,32 +23,20 @@ class LoginController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def signup
|
def signup
|
||||||
if User.find_all.empty? # signup the first user as admin
|
if User.find_all.empty? # the first user of the system
|
||||||
@page_title = "Sign up as the admin user"
|
@page_title = "Sign up as the admin user"
|
||||||
elsif session['user_id'] # we have someone logged in
|
@user = get_new_user
|
||||||
get_admin_user
|
|
||||||
if session['user_id'] == @admin.id # logged in user is admin, so allow signup
|
|
||||||
@page_title = "Sign up a new user"
|
|
||||||
else
|
|
||||||
@page_title = "No signups"
|
|
||||||
@admin_email = @admin.preferences["admin_email"]
|
|
||||||
render :action => "nosignup"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
else # no-one logged in, but we have some Users
|
|
||||||
get_admin_user
|
|
||||||
@page_title = "No signups"
|
|
||||||
@admin_email = @admin.preferences["admin_email"]
|
|
||||||
render :action => "nosignup"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if session['new_user']
|
|
||||||
@user = session['new_user']
|
|
||||||
session['new_user'] = nil
|
|
||||||
else
|
else
|
||||||
@user = User.new
|
admin = User.find_admin
|
||||||
end
|
if current_user_is admin
|
||||||
|
@page_title = "Sign up a new user"
|
||||||
|
@user = get_new_user
|
||||||
|
else # all other situations (i.e. a non-admin is logged in, or no one is logged in, but we have some users)
|
||||||
|
@page_title = "No signups"
|
||||||
|
@admin_email = admin.preferences["admin_email"]
|
||||||
|
render :action => "nosignup"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
@ -74,7 +58,7 @@ class LoginController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete
|
def delete
|
||||||
if params['id'] and ( params['id'] = @user.id or @user.is_admin )
|
if params['id'] and ( params['id'] == @user.id or @user.is_admin )
|
||||||
@user = User.find(params['id'])
|
@user = User.find(params['id'])
|
||||||
# TODO: Maybe it would be better to mark deleted. That way user deletes can be reversed.
|
# TODO: Maybe it would be better to mark deleted. That way user deletes can be reversed.
|
||||||
@user.destroy
|
@user.destroy
|
||||||
|
|
@ -93,12 +77,8 @@ class LoginController < ApplicationController
|
||||||
# Gets called by periodically_call_remote to check whether
|
# Gets called by periodically_call_remote to check whether
|
||||||
# the session has timed out yet
|
# the session has timed out yet
|
||||||
unless session == nil
|
unless session == nil
|
||||||
return if @controller_name == 'feed' or session['noexpiry'] == "on"
|
|
||||||
# If the method is called by the feed controller
|
|
||||||
# (which we don't have under session control)
|
|
||||||
# or if we checked the box to keep logged in on login
|
|
||||||
# then the session is not going to get called
|
|
||||||
if session
|
if session
|
||||||
|
return unless should_expire_sessions?
|
||||||
# Get expiry time (allow ten seconds window for the case where we have none)
|
# Get expiry time (allow ten seconds window for the case where we have none)
|
||||||
expiry_time = session['expiry_time'] || Time.now + 10
|
expiry_time = session['expiry_time'] || Time.now + 10
|
||||||
@time_left = expiry_time - Time.now
|
@time_left = expiry_time - Time.now
|
||||||
|
|
@ -111,4 +91,24 @@ class LoginController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def get_new_user
|
||||||
|
if session['new_user']
|
||||||
|
user = session['new_user']
|
||||||
|
session['new_user'] = nil
|
||||||
|
else
|
||||||
|
user = User.new
|
||||||
|
end
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user_is(user)
|
||||||
|
session['user_id'] && session['user_id'] == user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def should_expire_sessions?
|
||||||
|
session['noexpiry'] != "on"
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
class NoteController < ApplicationController
|
class NoteController < ApplicationController
|
||||||
|
|
||||||
model :user
|
model :user
|
||||||
before_filter :login_required
|
prepend_before_filter :login_required
|
||||||
|
|
||||||
layout "standard"
|
layout "standard"
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@all_notes = @user.notes
|
@all_notes = @user.notes
|
||||||
@page_title = "TRACKS::All notes"
|
@page_title = "TRACKS::All notes"
|
||||||
|
respond_to do |wants|
|
||||||
|
wants.html
|
||||||
|
wants.xml { render :xml => @all_notes.to_xml }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ class ProjectController < ApplicationController
|
||||||
model :todo
|
model :todo
|
||||||
|
|
||||||
helper :todo
|
helper :todo
|
||||||
before_filter :login_required
|
prepend_before_filter :login_required
|
||||||
|
|
||||||
layout "standard"
|
layout "standard"
|
||||||
|
|
||||||
|
|
@ -18,6 +18,10 @@ class ProjectController < ApplicationController
|
||||||
def list
|
def list
|
||||||
init
|
init
|
||||||
@page_title = "TRACKS::List Projects"
|
@page_title = "TRACKS::List Projects"
|
||||||
|
respond_to do |wants|
|
||||||
|
wants.html
|
||||||
|
wants.xml { render :xml => @projects.to_xml }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Filter the projects to show just the one passed in the URL
|
# Filter the projects to show just the one passed in the URL
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ class User < ActiveRecord::Base
|
||||||
def self.authenticate(login, pass)
|
def self.authenticate(login, pass)
|
||||||
find_first(["login = ? AND password = ?", login, sha1(pass)])
|
find_first(["login = ? AND password = ?", login, sha1(pass)])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.find_admin
|
||||||
|
find_first([ "is_admin = ?", true ])
|
||||||
|
end
|
||||||
|
|
||||||
def change_password(pass,pass_confirm)
|
def change_password(pass,pass_confirm)
|
||||||
self.password = pass
|
self.password = pass
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,12 @@ module LoginSystem
|
||||||
if session['user_id'] and authorize?(User.find(session['user_id']))
|
if session['user_id'] and authorize?(User.find(session['user_id']))
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
http_user, http_pass = get_basic_auth_data
|
||||||
|
if user = User.authenticate(http_user, http_pass)
|
||||||
|
session['user_id'] = user.id
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
# store current location so that we can
|
# store current location so that we can
|
||||||
# come back after the user logged in
|
# come back after the user logged in
|
||||||
|
|
@ -65,10 +71,10 @@ module LoginSystem
|
||||||
# example use :
|
# example use :
|
||||||
# a popup window might just close itself for instance
|
# a popup window might just close itself for instance
|
||||||
def access_denied
|
def access_denied
|
||||||
if request.xhr?
|
respond_to do |wants|
|
||||||
render :partial => 'login/redirect_to_login'
|
wants.html { redirect_to :controller=>"login", :action =>"login" }
|
||||||
else
|
wants.js { render :partial => 'login/redirect_to_login' }
|
||||||
redirect_to :controller=>"login", :action =>"login"
|
wants.xml { basic_auth_denied }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -87,5 +93,33 @@ module LoginSystem
|
||||||
session['return-to'] = nil
|
session['return-to'] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# HTTP Basic auth code adapted from Coda Hale's simple_http_auth plugin. Thanks, Coda!
|
||||||
|
def get_basic_auth_data
|
||||||
|
|
||||||
|
auth_locations = ['REDIRECT_REDIRECT_X_HTTP_AUTHORIZATION',
|
||||||
|
'REDIRECT_X_HTTP_AUTHORIZATION',
|
||||||
|
'X-HTTP_AUTHORIZATION', 'HTTP_AUTHORIZATION']
|
||||||
|
|
||||||
|
authdata = nil
|
||||||
|
for location in auth_locations
|
||||||
|
if request.env.has_key?(location)
|
||||||
|
authdata = request.env[location].to_s.split
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if authdata and authdata[0] == 'Basic'
|
||||||
|
user, pass = Base64.decode64(authdata[1]).split(':')[0..1]
|
||||||
|
else
|
||||||
|
user, pass = ['', '']
|
||||||
|
end
|
||||||
|
return user, pass
|
||||||
|
end
|
||||||
|
|
||||||
|
def basic_auth_denied
|
||||||
|
response.headers["Status"] = "Unauthorized"
|
||||||
|
response.headers["WWW-Authenticate"] = "Basic realm=\"'Tracks Login Required'\""
|
||||||
|
render :text => "401 Unauthorized: You are not authorized to interact with Tracks.", :status => 401
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.5 KiB |
Loading…
Add table
Add a link
Reference in a new issue