You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ode.apache.org by as...@apache.org on 2007/11/19 23:31:04 UTC
svn commit: r596466 - in /ode/sandbox/singleshot: ./ app/controllers/
app/helpers/ app/models/ app/views/layouts/ app/views/sessions/ config/
db/migrate/ doc/pages/ public/images/ public/stylesheets/
Author: assaf
Date: Mon Nov 19 14:31:03 2007
New Revision: 596466
URL: http://svn.apache.org/viewvc?rev=596466&view=rev
Log:
Added People model and way to login
Added:
ode/sandbox/singleshot/app/controllers/sessions_controller.rb
ode/sandbox/singleshot/app/controllers/tasks_controller.rb
ode/sandbox/singleshot/app/helpers/application_helper.rb
ode/sandbox/singleshot/app/models/person.rb
ode/sandbox/singleshot/app/models/task.rb
ode/sandbox/singleshot/app/views/layouts/application.html.erb
ode/sandbox/singleshot/app/views/sessions/
ode/sandbox/singleshot/app/views/sessions/show.html.erb
ode/sandbox/singleshot/db/migrate/001_create_people.rb
ode/sandbox/singleshot/doc/pages/
ode/sandbox/singleshot/doc/pages/authentication.textile
ode/sandbox/singleshot/public/images/submit.gif (with props)
ode/sandbox/singleshot/public/images/textfield.png (with props)
ode/sandbox/singleshot/public/stylesheets/default.css (with props)
Modified:
ode/sandbox/singleshot/README
ode/sandbox/singleshot/app/controllers/application.rb
ode/sandbox/singleshot/config/environment.rb
ode/sandbox/singleshot/config/routes.rb
Modified: ode/sandbox/singleshot/README
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/README?rev=596466&r1=596465&r2=596466&view=diff
==============================================================================
--- ode/sandbox/singleshot/README (original)
+++ ode/sandbox/singleshot/README Mon Nov 19 14:31:03 2007
@@ -6,6 +6,7 @@
$ cd singleshot
$ rake rails:freeze:edge
+ $ sudo gem install bcrypt-ruby
== Setting up the database
@@ -24,4 +25,16 @@
$ rake db:create:all
$ rake db:migrate
$ rake db:test:clone
+
+
+== Adding people
+
+At the moment you can only add new people to the database using the Rails
+console. The minimum information required for each person is a valid e-mail
+address and password. Their nickname (used for login) and full name are
+extracted from the e-mail address, but you can set those separately:
+
+ $ ./script/console
+ >> Person.create :email=>'assaf@apache.org', :password=>'secret'
+ => #<Person id: 1, nickname: "assaf", fullname: "Assaf", email: "assaf@apache.org", language: nil, timezone: nil, password: "$2a$10$ncrCapBySVOw737CD9EnK.XEEFd1hx4v6SVbGQpd85K0...", access_key: "668041114980ae1b3bffda84d6e036e9", site_url: nil, created_at: "2007-11-19 12:40:18", updated_at: "2007-11-19 12:40:18">
Modified: ode/sandbox/singleshot/app/controllers/application.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/controllers/application.rb?rev=596466&r1=596465&r2=596466&view=diff
==============================================================================
--- ode/sandbox/singleshot/app/controllers/application.rb (original)
+++ ode/sandbox/singleshot/app/controllers/application.rb Mon Nov 19 14:31:03 2007
@@ -4,5 +4,88 @@
class ApplicationController < ActionController::Base
helper :all # include all helpers, all the time
+ # Turn sessions off for JSON, XML, Atom, etc. We only use sessions for login/flash in HTML pages
+ # and XHR requests.
+ session :if=>lambda { |req| !(req.format.html? || req.xhr?) }
+
+ # Methods dealing with authentication.
+ module Authentication
+
+ protected
+
+ # Returns Person object for currently authenticated user.
+ attr_reader :authenticated
+
+ def authenticate
+ if request.format.html?
+ authenticate_using_session
+ else
+ authenticate_using_http_basic
+ end
+ end
+
+ # Use HTTP Basic authentication.
+ def authenticate_using_http_basic
+ authenticate_or_request_with_http_basic do |login, password|
+ @authenticated = Person.authenticate(login, password)
+ end
+ end
+
+ # Use session-based authentication. If not currently authenticated in session,
+ # redirects to session_url. Set person_id on login, reset on logout.
+ def authenticate_using_session
+ @authenticated = Person.find_by_id(session[:person_id])
+ unless @authenticated
+ flash[:follow] = request.url
+ redirect_to(session_url)
+ end
+ end
+
+ # Authenticate using access_key parameter for cases where we cannot use sessions,
+ # HTTP Basic or any other mechanism, for example, for accessing feeds and iCalendar.
+ # This authentication only works for GET requests.
+ def authenticate_using_access_key
+ @authenticated = Person.find_by_access_key(params[:access_key]) if request.get?
+ head :not_found unless @authenticated
+ end
+
+ # Returns query string parameters for authentication, see #authenticate_using_access_key.
+ def access_key_authentication_parameters
+ { :access_key=>authenticated.access_key }
+ end
+
+ end
+
+
+ # Methods dealing with the authenticated user.
+ module Authenticated
+
+ protected
+
+ # Returns language code for currently authenticated user (may be nil).
+ def language
+ authenticated.language
+ end
+
+ # Given the time (defaults to now) returns an adjusted time based on the authenticated user's timezone.
+ # This should be used when presenting formatted time without the timezone.
+ def tz_adjust(time = Time.now)
+ return time unless authenticated.timezone
+ timezone = TimeZone[authenticated.timezone]
+ timezone ? timezone.adjust(time) : time
+ end
+
+ # Given the time (Time or string) returns unadjusted time (to UTC) based on the authenticated user's timezone.
+ # This should be used for time inputs provided without the timezone.
+ def tz_unadjust(time)
+ time = Time.parse(time.to_s) unless Time === time
+ return time unless authenticated.timezone
+ timezone = TimeZone[authenticated.timezone]
+ timezone ? timezone.unadjust(time) : time
+ end
+
+ end
+
+ include Authentication, Authenticated
end
Added: ode/sandbox/singleshot/app/controllers/sessions_controller.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/controllers/sessions_controller.rb?rev=596466&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/controllers/sessions_controller.rb (added)
+++ ode/sandbox/singleshot/app/controllers/sessions_controller.rb Mon Nov 19 14:31:03 2007
@@ -0,0 +1,23 @@
+class SessionsController < ApplicationController
+
+ def show
+ flash.keep(:follow)
+ end
+
+ def create
+ if person = Person.authenticate(params[:login], params[:password])
+ session[:person_id] = person.id
+ redirect_to flash[:follow] || root_url, :status=>:see_other
+ else
+ flash.keep(:follow)
+ flash[:error] = 'No account with this login and password.'
+ redirect_to session_url, :status=>:see_other
+ end
+ end
+
+ def destroy
+ reset_session
+ redirect_to root_url, :status=>:see_other
+ end
+
+end
Added: ode/sandbox/singleshot/app/controllers/tasks_controller.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/controllers/tasks_controller.rb?rev=596466&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/controllers/tasks_controller.rb (added)
+++ ode/sandbox/singleshot/app/controllers/tasks_controller.rb Mon Nov 19 14:31:03 2007
@@ -0,0 +1,3 @@
+class TasksController < ApplicationController
+
+end
Added: ode/sandbox/singleshot/app/helpers/application_helper.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/helpers/application_helper.rb?rev=596466&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/helpers/application_helper.rb (added)
+++ ode/sandbox/singleshot/app/helpers/application_helper.rb Mon Nov 19 14:31:03 2007
@@ -0,0 +1,13 @@
+# Methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+
+ # Returns a link to a person using their full name as the link text and site URL
+ # (or profile, if unspecified) as the reference.
+ def link_to_person(person, options = nil)
+ options ||= {}
+ person.site_url ?
+ link_to(h(person.fullname), person.site_url, options.reverse_merge(:title=>"See #{person.fullname}'s profile")) :
+ content_tag('span', h(person.fullname), options)
+ end
+
+end
Added: ode/sandbox/singleshot/app/models/person.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/models/person.rb?rev=596466&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/models/person.rb (added)
+++ ode/sandbox/singleshot/app/models/person.rb Mon Nov 19 14:31:03 2007
@@ -0,0 +1,108 @@
+# == Schema Information
+# Schema version: 1
+#
+# Table name: people
+#
+# id :integer(11) not null, primary key
+# nickname :string(255) default(""), not null
+# fullname :string(255) default(""), not null
+# email :string(255) default(""), not null
+# language :string(5)
+# timezone :integer(4)
+# password :string(64)
+# created_at :datetime
+# updated_at :datetime
+#
+
+require 'md5'
+require 'bcrypt'
+
+# Internally we keep a primary key association between the person and various other records.
+# Externally, we use a public identifier returned from #to_param and resolved with Person.identify.
+# The base implementation uses the person's nickname as the public identifier.
+#
+# In addition, we need to know the person's full name for presentation and e-mail address for
+# sending notifications. Language and timezone (in minutes relative to UTC) are optional,
+# although it is highly recommended to set the timezone.
+#
+# Passwords are optional, since not every one requires password authentication (may use OpenID,
+# or some other authentication mechanism). We use BCrype to store passwords securely.
+#
+# The access_key field is used for authentication when sessions and HTTP Basic are not possible,
+# for example, to access feeds and iCal.
+class Person < ActiveRecord::Base
+
+ class << self
+
+ # Resolves a person based on their identity, throws RecordNotFound if no such person can
+ # be found. For convenience, when called with a Person object, returns that same object.
+ # Matches against the identity returned in to_param.
+ def identify(identity)
+ return identity if Person === identity
+ Person.find_by_nickname(identity) or
+ raise ActiveRecord::RecordNotFound, "Cannot find '#{identity}'"
+ end
+
+ # Used for identity/password authentication. Return the person if authenticated.
+ def authenticate(login, password)
+ person = Person.find_by_nickname(login)
+ person if person && person.password?(password)
+ end
+
+ end
+
+ def initialize(*args)
+ super
+ self.access_key!
+ end
+
+ # Returns an identifier suitable for use with Person.resolve.
+ def to_param
+ nickname
+ end
+
+ # Must have nickname.
+ validates_uniqueness_of :nickname, :message=>'A person with this nickname already exists.'
+ before_validation do |record|
+ record.nickname = record.email.to_s.strip[/([^\s@]*)/, 1].downcase if record.nickname.blank?
+ record.nickname = record.nickname.gsub(/\s+/, '').downcase
+ end
+
+ # Must have fullname.
+ before_validation do |record|
+ record.fullname = record.nickname.to_s.split(/[_.]+/).map(&:capitalize).join(' ') if record.fullname.blank?
+ record.fullname = record.fullname.strip.gsub(/\s+/, ' ')
+ end
+
+ # Must have e-mail address.
+ validates_format_of :email, :with => /^([^@\s]+)@[-a-z0-9]+(\.[-a-z0-9]+)*$/, :message=>"I need a valid e-mail address."
+ validates_uniqueness_of :email, :message=>'This e-mail is already in use.'
+ before_validation do |record|
+ record.email = record.email.to_s.strip.downcase
+ end
+
+ # TODO: Some way to check minimum size of passwords.
+
+ def password=(password)
+ write_attribute :password, BCrypt::Password.create(password )
+ end
+
+ def password?(password)
+ BCrypt::Password.new(read_attribute(:password)) == password
+ end
+
+ def reset_password!
+ password = Array.new(10).map { (65 + rand(58)).chr }.join
+ self.password = password
+ save!
+ password
+ end
+
+ attr_protected :access_key
+
+ def access_key!
+ self.access_key = MD5.hexdigest(OpenSSL::Random.random_bytes(128))
+ save! unless new_record?
+ end
+
+end
Added: ode/sandbox/singleshot/app/models/task.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/models/task.rb?rev=596466&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/models/task.rb (added)
+++ ode/sandbox/singleshot/app/models/task.rb Mon Nov 19 14:31:03 2007
@@ -0,0 +1,25 @@
+# == Schema Information
+# Schema version: 1
+#
+# Table name: tasks
+#
+# id :integer(11) not null, primary key
+# name :string(255) default(""), not null
+# title :string(4000) default(""), not null
+# priority :integer(11)
+# task_url :string(2000)
+# status :integer(2) default(0), not null
+# complete_at :datetime
+# completion_url :string(2000)
+# completion_format :string(255)
+# access_key :string(32)
+# data :text default(""), not null
+# version :integer(11) default(0), not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+class Task < ActiveRecord::Base
+
+
+end
Added: ode/sandbox/singleshot/app/views/layouts/application.html.erb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/views/layouts/application.html.erb?rev=596466&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/views/layouts/application.html.erb (added)
+++ ode/sandbox/singleshot/app/views/layouts/application.html.erb Mon Nov 19 14:31:03 2007
@@ -0,0 +1,20 @@
+<html>
+ <head>
+ <title>Single Shot<%= " – #{escape_once(@title)}" if @title %></title>
+ <%= javascript_include_tag :all, :cache=>true %>
+ <%= stylesheet_link_tag :all, :cache=>true %>
+ <% @alternate.each do |mime, url| %>
+ <%= auto_discovery_link_tag mime.to_sym, url %>
+ <% end if @alternate %>
+ </head>
+ <body>
+ <div id='header'>
+ </div>
+ <div id='main'>
+<%= yield %>
+ </div>
+ <div id='footer'>
+ <!-- Footer comes here -->
+ </div>
+ </body>
+</html>
Added: ode/sandbox/singleshot/app/views/sessions/show.html.erb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/views/sessions/show.html.erb?rev=596466&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/views/sessions/show.html.erb (added)
+++ ode/sandbox/singleshot/app/views/sessions/show.html.erb Mon Nov 19 14:31:03 2007
@@ -0,0 +1,13 @@
+<div class='login'>
+ <% form_for :session, :url=>session_url do |f| %>
+ <%= content_tag 'p', flash[:error], :class=>'error' if flash[:error] %>
+ <dl>
+ <dt><label for='login'>Login:</label></dt>
+ <dd><%= text_field_tag 'login', nil, :size=>30 %></dd>
+ <dt><label for='password'>Password:</label></dt>
+ <dd><%= password_field_tag 'password', nil, :size=>30 %></dd>
+ <dd><%= submit_tag 'Login' %></dd>
+ </dl>
+ <% end %>
+</div>
+<%= javascript_tag "$('login').focus()" %>
Modified: ode/sandbox/singleshot/config/environment.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/config/environment.rb?rev=596466&r1=596465&r2=596466&view=diff
==============================================================================
--- ode/sandbox/singleshot/config/environment.rb (original)
+++ ode/sandbox/singleshot/config/environment.rb Mon Nov 19 14:31:03 2007
@@ -10,6 +10,7 @@
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')
+gem 'bcrypt-ruby', '~>2.0'
Rails::Initializer.run do |config|
# Settings in config/environments/* take precedence over those specified here
Modified: ode/sandbox/singleshot/config/routes.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/config/routes.rb?rev=596466&r1=596465&r2=596466&view=diff
==============================================================================
--- ode/sandbox/singleshot/config/routes.rb (original)
+++ ode/sandbox/singleshot/config/routes.rb Mon Nov 19 14:31:03 2007
@@ -1,5 +1,8 @@
ActionController::Routing::Routes.draw do |map|
+ map.resource :session
+ map.root :controller=>'tasks'
+
# Authentication resource (create/destroy).
# The priority is based upon order of creation: first created -> highest priority.
Added: ode/sandbox/singleshot/db/migrate/001_create_people.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/db/migrate/001_create_people.rb?rev=596466&view=auto
==============================================================================
--- ode/sandbox/singleshot/db/migrate/001_create_people.rb (added)
+++ ode/sandbox/singleshot/db/migrate/001_create_people.rb Mon Nov 19 14:31:03 2007
@@ -0,0 +1,23 @@
+class CreatePeople < ActiveRecord::Migration
+ def self.up
+ create_table :people do |t|
+ t.string :nickname, :null=>false
+ t.string :fullname, :null=>false
+ t.string :email, :null=>false
+ t.string :language, :null=>true, :limit=>5
+ t.integer :timezone, :null=>true, :limit=>4
+ t.string :password, :null=>true, :limit=>64
+ t.string :access_key, :null=>false, :limit=>32
+ t.string :site_url, :null=>true
+ t.timestamps
+ end
+ add_index :people, :nickname, :unique=>true
+ add_index :people, :fullname
+ add_index :people, :email, :unique=>true
+ add_index :people, :access_key, :unique=>true
+ end
+
+ def self.down
+ drop_table :people
+ end
+end
Added: ode/sandbox/singleshot/doc/pages/authentication.textile
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/doc/pages/authentication.textile?rev=596466&view=auto
==============================================================================
--- ode/sandbox/singleshot/doc/pages/authentication.textile (added)
+++ ode/sandbox/singleshot/doc/pages/authentication.textile Mon Nov 19 14:31:03 2007
@@ -0,0 +1,44 @@
+h1. Access Control and Security
+
+This section deals with people, authentication, authorization and all other manners of security.
+
+
+h2. People
+
+Every person is represented with a record in the database (the @persons@ table). The minimum information we need to know about each person is their identity (lookup and reference), full name (display), e-mail address (notifications), preferred language and current timezone.
+
+The implementation uses nicknames for identity. You can change the implementation to use other forms of identity (e.g. OpenID URLs) by changing @Person.identify@ and @Person.to_param@ respectively. It also uses nicknames as login names that in combination with passwords are used for HTTP Basic and session authentication. You can change this behavior or implement other authentication mechanisms by changing @Person.authenticate@.
+
+The last piece of information we use is the site_url, which if specified will link to that person's HTML page.
+
+Storing information about people externally (e.g. LDAP directory, OpenID server) is possible with the following constraints. You can replace the way @Person@ retrieves, updates and authenticates people in the system, but you need to maintain at least a skeleton record referencing those external resource. Other records reference people internally through their primary key.
+
+
+h2. Authentication
+
+Singleshot requires at least four different methods of authentication, depending on the type of access. Username/password authentication (HTTP Basic and Sessions) is built into the system, but doesn't preclude the use of external mechanisms such as Single Sign-on, OpenID and OAuth.
+
+*HTTP Basic* The REST API uses HTTP Basic Authentication. HTTP Basic is generally easier to use and more widely supported than HTTP Digest. Passing sensitive information in clear text is a concern but not a reason for using HTTP Digest; use HTTPS to secure all information transmitted over the wire.
+
+Passwords are stored in crypted form using BCrypt which adds a salt before hashing the password. BCrypt uses a computationally intensive algorithm derived from Blowfish that is harder to break than MD5. Crypted passwords are 64 characters long. Clear text passwords are distinguishable to their first 70 characters.
+
+*Sessions* HTTP Authentication is not particularly user friendly. The presentation of the authentication dialog box is inconsistent with the rest of the application, and provides no relevant links for registration, retrieving lost passwords or using alternative authentication mechanisms (such as OpenID). It also does not integrate with single sign-on.
+
+Browsers authenticate through several mechanisms that eventually associate a particular user with the current session and maintain that authentication using session cookies. The current mechanism uses login name and password.
+
+*Access key* Feed readers and calendars can't use either scheme. Some do allow you to use URLs that contain authentication information (username and password), but those are not implemented consistently, and further risks exposing your credentials. Instead we use an access key generated for each account. This access key is used as a query string parameter to grant read-only (GET) access to the resource.
+
+Resources that support the access key create their URLs using @access_key_authentication_parameters@ and authenticate using @authenticate_using_access_key@. To deal with compromised access keys, reset the person's access key by calling @Person.access_key!@.
+
+*Tokens* To view and perform the task, the application also needs to access the task state. Again we have no way of passing credentials around, and instead use unique tokens to grant access to the individual task and stakeholder.
+
+*OpenID and OAuth* TBD.
+
+*Single Sign-on* TBD.
+
+
+h2. Authorization
+
+
+h2. HTTPS
+
Added: ode/sandbox/singleshot/public/images/submit.gif
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/public/images/submit.gif?rev=596466&view=auto
==============================================================================
Binary file - no diff available.
Propchange: ode/sandbox/singleshot/public/images/submit.gif
------------------------------------------------------------------------------
svn:mime-type = image/gif
Added: ode/sandbox/singleshot/public/images/textfield.png
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/public/images/textfield.png?rev=596466&view=auto
==============================================================================
Binary file - no diff available.
Propchange: ode/sandbox/singleshot/public/images/textfield.png
------------------------------------------------------------------------------
svn:executable = *
Propchange: ode/sandbox/singleshot/public/images/textfield.png
------------------------------------------------------------------------------
svn:mime-type = image/png
Added: ode/sandbox/singleshot/public/stylesheets/default.css
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/public/stylesheets/default.css?rev=596466&view=auto
==============================================================================
--- ode/sandbox/singleshot/public/stylesheets/default.css (added)
+++ ode/sandbox/singleshot/public/stylesheets/default.css Mon Nov 19 14:31:03 2007
@@ -0,0 +1,111 @@
+body {
+ color: #000;
+ background-color: #fff;
+ font-family: Tahoma, Arial, Helvetica, sans-serif;
+ line-height: 140%;
+ margin: 0;
+ padding: 0;
+}
+
+a:link, a:visited{
+ color: #046380;
+ text-decoration: none;
+}
+
+a:hover{
+ color: #069BC9;
+ text-decoration: underline;
+}
+
+img {
+ border: none;
+}
+
+abbr {
+ cursor: help;
+ border: none;
+}
+
+/** Form controls **/
+
+input[type=text], input[type=password] {
+ background: transparent url(/images/textfield.png) bottom left repeat-x;
+ padding: 0.25em 0.5em;
+ border: 1px solid #ccc;
+ margin: 0;
+}
+input[type=text]:hover, input[type=password]:hover, select:hover {
+ border: 1px solid #888;
+}
+input[type=text]:focus, input[type=password]:focus, select:focus {
+ border: 1px solid #000;
+}
+select {
+ padding: 0.25em;
+ border: 1px outset #ccc;
+}
+input[type=submit], input[type=reset], button {
+ border: outset 1px #ccc;
+ background: #999;
+ color: #444;
+ padding: 0.1em 0.2em 0.1em 0.2em;
+ margin: 0;
+ background: url(/images/submit.gif) repeat-x left top;
+ text-decoration: none;
+}
+input[type=submit]:disabled, input[type=reset]:disabled, button:disabled {
+ color: #ccc;
+ cursor: default;
+}
+option {
+ margin-top: 0.3em;
+}
+optgroup {
+ margin-bottom: 0.3em;
+ padding-bottom: 0.3em;
+ border-bottom: 1px solid #ccc;
+}
+form.button-to {
+ display: inline;
+}
+form.button-to div {
+ display: inline;
+}
+
+
+/** Header **/
+
+#header {
+}
+
+
+/** Main **/
+
+#main {
+}
+
+
+/** Footer **/
+
+#footer {
+}
+
+
+/** Login **/
+div.login {
+ width: 25em;
+ margin: 2em auto 2em auto;
+ border: 1px solid #ccc;
+ padding: 1em 2em 2em 2em;
+}
+div.login dt {
+ width: 6em;
+ float: left;
+ font-weight: bolder;
+}
+div.login dd {
+ margin: 0 0 1em 7em;
+}
+div.login .error {
+ color: red;
+}
Propchange: ode/sandbox/singleshot/public/stylesheets/default.css
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: ode/sandbox/singleshot/public/stylesheets/default.css
------------------------------------------------------------------------------
svn:keywords = Date Revision
Propchange: ode/sandbox/singleshot/public/stylesheets/default.css
------------------------------------------------------------------------------
svn:mime-type = text/css