tpitale-rack-oauth2-server 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. data/CHANGELOG +202 -0
  2. data/Gemfile +16 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +604 -0
  5. data/Rakefile +90 -0
  6. data/VERSION +1 -0
  7. data/bin/oauth2-server +206 -0
  8. data/lib/rack-oauth2-server.rb +4 -0
  9. data/lib/rack/oauth2/admin/css/screen.css +347 -0
  10. data/lib/rack/oauth2/admin/images/loading.gif +0 -0
  11. data/lib/rack/oauth2/admin/images/oauth-2.png +0 -0
  12. data/lib/rack/oauth2/admin/js/application.coffee +220 -0
  13. data/lib/rack/oauth2/admin/js/jquery.js +166 -0
  14. data/lib/rack/oauth2/admin/js/jquery.tmpl.js +414 -0
  15. data/lib/rack/oauth2/admin/js/protovis-r3.2.js +277 -0
  16. data/lib/rack/oauth2/admin/js/sammy.js +5 -0
  17. data/lib/rack/oauth2/admin/js/sammy.json.js +5 -0
  18. data/lib/rack/oauth2/admin/js/sammy.oauth2.js +142 -0
  19. data/lib/rack/oauth2/admin/js/sammy.storage.js +5 -0
  20. data/lib/rack/oauth2/admin/js/sammy.title.js +5 -0
  21. data/lib/rack/oauth2/admin/js/sammy.tmpl.js +5 -0
  22. data/lib/rack/oauth2/admin/js/underscore.js +722 -0
  23. data/lib/rack/oauth2/admin/views/client.tmpl +58 -0
  24. data/lib/rack/oauth2/admin/views/clients.tmpl +52 -0
  25. data/lib/rack/oauth2/admin/views/edit.tmpl +80 -0
  26. data/lib/rack/oauth2/admin/views/index.html +39 -0
  27. data/lib/rack/oauth2/admin/views/no_access.tmpl +4 -0
  28. data/lib/rack/oauth2/models.rb +27 -0
  29. data/lib/rack/oauth2/models/access_grant.rb +54 -0
  30. data/lib/rack/oauth2/models/access_token.rb +129 -0
  31. data/lib/rack/oauth2/models/auth_request.rb +61 -0
  32. data/lib/rack/oauth2/models/client.rb +93 -0
  33. data/lib/rack/oauth2/rails.rb +105 -0
  34. data/lib/rack/oauth2/server.rb +458 -0
  35. data/lib/rack/oauth2/server/admin.rb +250 -0
  36. data/lib/rack/oauth2/server/errors.rb +104 -0
  37. data/lib/rack/oauth2/server/helper.rb +147 -0
  38. data/lib/rack/oauth2/server/practice.rb +79 -0
  39. data/lib/rack/oauth2/server/railtie.rb +24 -0
  40. data/lib/rack/oauth2/server/utils.rb +30 -0
  41. data/lib/rack/oauth2/sinatra.rb +71 -0
  42. data/rack-oauth2-server.gemspec +24 -0
  43. data/rails/init.rb +11 -0
  44. data/test/admin/api_test.rb +228 -0
  45. data/test/admin/ui_test.rb +38 -0
  46. data/test/oauth/access_grant_test.rb +276 -0
  47. data/test/oauth/access_token_test.rb +311 -0
  48. data/test/oauth/authorization_test.rb +298 -0
  49. data/test/oauth/server_methods_test.rb +292 -0
  50. data/test/rails2/app/controllers/api_controller.rb +40 -0
  51. data/test/rails2/app/controllers/application_controller.rb +2 -0
  52. data/test/rails2/app/controllers/oauth_controller.rb +17 -0
  53. data/test/rails2/config/environment.rb +19 -0
  54. data/test/rails2/config/environments/test.rb +0 -0
  55. data/test/rails2/config/routes.rb +13 -0
  56. data/test/rails3/app/controllers/api_controller.rb +40 -0
  57. data/test/rails3/app/controllers/application_controller.rb +2 -0
  58. data/test/rails3/app/controllers/oauth_controller.rb +17 -0
  59. data/test/rails3/config/application.rb +19 -0
  60. data/test/rails3/config/environment.rb +2 -0
  61. data/test/rails3/config/routes.rb +12 -0
  62. data/test/setup.rb +120 -0
  63. data/test/sinatra/my_app.rb +69 -0
  64. metadata +145 -0
@@ -0,0 +1,90 @@
1
+ require "rake/testtask"
2
+
3
+ spec = Gem::Specification.load(Dir["*.gemspec"].first)
4
+
5
+ desc "Install dependencies"
6
+ task :setup do
7
+ puts "Installing gems for testing with Sinatra ..."
8
+ sh "bundle install"
9
+ puts "Installing gems for testing with Rails 2.3 ..."
10
+ sh "env BUNDLE_GEMFILE=Rails2 bundle install"
11
+ puts "Installing gems for testing with Rails 3.x ..."
12
+ sh "env BUNDLE_GEMFILE=Rails3 bundle install"
13
+ end
14
+
15
+ desc "Run this in development mode when updating the CoffeeScript file"
16
+ task :coffee do
17
+ sh "coffee -w -o lib/rack/oauth2/admin/js/ lib/rack/oauth2/admin/js/application.coffee"
18
+ end
19
+
20
+ task :compile do
21
+ sh "coffee -c -l -o lib/rack/oauth2/admin/js/ lib/rack/oauth2/admin/js/application.coffee"
22
+ end
23
+
24
+ desc "Build the Gem"
25
+ task :build=>:compile do
26
+ sh "gem build #{spec.name}.gemspec"
27
+ end
28
+
29
+ desc "Install #{spec.name} locally"
30
+ task :install=>:build do
31
+ sudo = "sudo" unless File.writable?( Gem::ConfigMap[:bindir])
32
+ sh "#{sudo} gem install #{spec.name}-#{spec.version}.gem"
33
+ end
34
+
35
+ desc "Push new release to gemcutter and git tag"
36
+ task :push=>["test:all", "build"] do
37
+ sh "git push"
38
+ puts "Tagging version #{spec.version} .."
39
+ sh "git tag v#{spec.version}"
40
+ sh "git push --tag"
41
+ puts "Building and pushing gem .."
42
+ sh "gem push #{spec.name}-#{spec.version}.gem"
43
+ end
44
+
45
+ desc "Run all tests"
46
+ Rake::TestTask.new do |task|
47
+ task.test_files = FileList['test/**/*_test.rb']
48
+ if Rake.application.options.trace
49
+ #task.warning = true
50
+ task.verbose = true
51
+ elsif Rake.application.options.silent
52
+ task.ruby_opts << "-W0"
53
+ else
54
+ task.verbose = true
55
+ end
56
+ task.ruby_opts << "-I."
57
+ end
58
+
59
+ namespace :test do
60
+ task :all=>["test:sinatra", "test:rails2", "test:rails3"]
61
+ desc "Run all tests against Sinatra"
62
+ task :sinatra do
63
+ sh "rake test FRAMEWORK=sinatra"
64
+ end
65
+ desc "Run all tests against Rails"
66
+ task :rails do
67
+ sh "rake test FRAMEWORK=rails"
68
+ end
69
+ desc "Run all tests against Rails 2.3.x"
70
+ task :rails2 do
71
+ sh "env BUNDLE_GEMFILE=Rails2 rake test FRAMEWORK=rails"
72
+ end
73
+ desc "Run all tests against Rails 3.x"
74
+ task :rails3 do
75
+ sh "env BUNDLE_GEMFILE=Rails3 bundle exec rake test FRAMEWORK=rails"
76
+ end
77
+ end
78
+ task :default=>"test:all"
79
+
80
+ begin
81
+ require "yard"
82
+ YARD::Rake::YardocTask.new do |doc|
83
+ doc.files = FileList["lib/**/*.rb"]
84
+ end
85
+ rescue LoadError
86
+ end
87
+
88
+ task :clean do
89
+ rm_rf %w{doc .yardoc *.gem}
90
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.2.1
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+ require "rack/oauth2/server"
4
+ require "uri"
5
+
6
+ # this is a weird way to shorten the namespace
7
+ include Rack::OAuth2
8
+
9
+
10
+ # if (i = ARGV.index("--db")) && ARGV[i+1]
11
+ # url = ARGV[i + 1]
12
+ # uri = URI.parse(url)
13
+ # uri = URI.parse("mongo://#{url}") if uri.opaque
14
+ # db = Mongo::Connection.new(uri.host, uri.port)[uri.path.sub(/^\//, "")]
15
+ # db.authenticate uri.user, uri.password if uri.user
16
+ # Server.database = db
17
+ # ARGV[i,2] = []
18
+ # end
19
+ if (i = ARGV.index("--port") || ARGV.index("-p")) && ARGV[i+1]
20
+ port = ARGV[i + 1].to_i
21
+ ARGV[i,2] = []
22
+ end
23
+
24
+
25
+ case ARGV[0]
26
+ when "list"
27
+
28
+ # fail "No database. Use the --db option to tell us which database to use" unless Server.database
29
+ Server::Client.all.each do |client|
30
+ next if client.revoked
31
+ print "%-30s\t%s\n" % [client.display_name, client.link]
32
+ print " ID %s\tSecret %s\n" % [client.id, client.secret]
33
+ print "\n"
34
+ end
35
+
36
+ when "register"
37
+
38
+ # fail "No database. Use the --db option to tell us which database to use" unless Server.database
39
+ begin
40
+ print "Application name:\t"
41
+ display_name = $stdin.gets
42
+ print "Application URL:\t"
43
+ link = $stdin.gets
44
+ print "Redirect URI:\t\t"
45
+ redirect_uri = $stdin.gets
46
+ print "Scope (space separated names):\t\t"
47
+ scope = $stdin.gets
48
+ client = Server::Client.create(:display_name=>display_name, :link=>link, :redirect_uri=>redirect_uri, :scope=>scope)
49
+ rescue
50
+ puts "\nFailed to register client: #{$!}"
51
+ exit -1
52
+ end
53
+ puts "Registered #{client.display_name}"
54
+ puts "ID\t#{client.id}"
55
+ puts "Secret\t#{client.secret}"
56
+
57
+ when "setup"
58
+
59
+ # fail "No database. Use the --db option to tell us which database to use" unless Server.database
60
+ puts "Where would you mount the Web console? This is a URL that must end with /admin,"
61
+ puts "for example, http://example.com/oauth/admin"
62
+ print ": "
63
+ uri = URI.parse($stdin.gets)
64
+ begin
65
+ uri.normalize!
66
+ fail "No an HTTP/S URL" unless uri.absolute? && %{http https}.include?(uri.scheme)
67
+ fail "Path must end with /admin" unless uri.path[/\/admin$/]
68
+ client = Server::Client.create(:display_name=>"OAuth Console", :link=>uri.to_s, :image_url=>"#{uri.to_s}/images/oauth-2.png",
69
+ :redirect_uri=>uri.to_s, :scope=>"oauth-admin")
70
+ rescue
71
+ puts "\nFailed to register client: #{$!}"
72
+ exit -1
73
+ end
74
+ print <<-TEXT
75
+
76
+ Next Steps
77
+ ==========
78
+
79
+ Make sure you ONLY authorize administrators to use the oauth-admin scope.
80
+ For example:
81
+
82
+ before_filter do
83
+ # Only admins allowed to authorize the scope oauth-admin
84
+ head oauth.deny! if oauth.scope.include?("oauth-admin") && !current_user.admin?
85
+ end
86
+
87
+ Rails 2.x, add the following to config/environment.rb:
88
+
89
+ config.after_initialize do
90
+ config.middleware.use Rack::OAuth2::Server::Admin.mount "#{uri.path}"
91
+ Rack::OAuth2::Server::Admin.set :client_id, "#{client.id}"
92
+ Rack::OAuth2::Server::Admin.set :client_secret, "#{client.secret}"
93
+ end
94
+
95
+ Rails 3.x, add the following to config/application.rb:
96
+
97
+ config.after_initialize do
98
+ Rack::OAuth2::Server::Admin.set :client_id, "#{client.id}"
99
+ Rack::OAuth2::Server::Admin.set :client_secret, "#{client.secret}"
100
+ end
101
+
102
+ And add the follownig to config/routes.rb:
103
+
104
+ mount Rack::OAuth2::Server::Admin=>"/oauth/admin"
105
+
106
+ Sinatra, Padrino and other Rack applications, mount the console:
107
+
108
+ Rack::Builder.new do
109
+ map("#{uri.path}") { run Rack::OAuth2::Server::Admin }
110
+ map("/") { run MyApp }
111
+ end
112
+ Rack::OAuth2::Server::Admin.set :client_id, "#{client.id}"
113
+ Rack::OAuth2::Server::Admin.set :client_secret, "#{client.secret}"
114
+
115
+ The console will authorize access by redirecting to
116
+ https://#{uri.host}/oauth/authorize
117
+
118
+ If this is not your OAuth 2.0 authorization endpoint, you can change it by
119
+ setting the :authorize option.
120
+ TEXT
121
+
122
+ when "practice"
123
+
124
+ require "logger"
125
+ begin
126
+ require "thin"
127
+ rescue LoadError
128
+ puts "Needs the Thin Web server. Please gem install thin and run again"
129
+ exit -1
130
+ end
131
+ require "rack/oauth2/server/practice"
132
+
133
+ # fail "No database. Use the --db option to tell us which database to use" unless Server.database
134
+ port ||= 8080
135
+ admin_url = "http://localhost:#{port}/oauth/admin"
136
+ unless client = Server::Client.lookup(admin_url)
137
+ client = Server::Client.create(:display_name=>"Practice OAuth Console", :image_url=>"#{admin_url}/images/oauth-2.png",
138
+ :link=>admin_url, :redirect_uri=>admin_url, :scope=>"oauth-admin")
139
+ end
140
+ Server::Admin.configure do |config|
141
+ logger = Logger.new(STDOUT)
142
+ logger.level = Logger::DEBUG
143
+ config.set :client_id, client.id
144
+ config.set :client_secret, client.secret
145
+ config.set :scope, "nobody sudo"
146
+ config.set :logger, logger
147
+ config.set :logging, true
148
+ config.set :dump_errors, true
149
+ config.oauth.logger = logger
150
+ end
151
+
152
+ Server::Practice.configure do |config|
153
+ logger = Logger.new(STDOUT)
154
+ logger.level = Logger::DEBUG
155
+ config.set :logger, logger
156
+ config.set :logging, true
157
+ config.set :dump_errors, true
158
+ config.oauth.logger = logger
159
+ end
160
+
161
+ print "\nFiring up the practice server.\nFor instructions, go to http://localhost:#{port}/\n\n\n"
162
+ Thin::Server.new "127.0.0.1", port do
163
+ map("/") { run Server::Practice.new }
164
+ map("/oauth/admin") { run Server::Admin.new }
165
+ end.start
166
+
167
+ # when "migrate"
168
+ #
169
+ # # fail "No database. Use the --db option to tell us which database to use" unless Server.database
170
+ # puts "Set all clients to this scope (can change later by calling Client.register):"
171
+ # print ": "
172
+ # scope = $stdin.gets.strip.split
173
+ # puts "Updating Client scope to #{scope.join(", ")}"
174
+ # Server::Client.collection.find({ :scope=>{ :$exists=>false } }, :fields=>[]).each do |client|
175
+ # update = { :scope=>scope,
176
+ # :tokens_granted=>Server::AccessToken.count(:client_id=>client["_id"]),
177
+ # :tokens_revoked=>Server::AccessToken.count(:client_id=>client["_id"], :revoked=>true) }
178
+ # Server::Client.collection.update({ :_id=>client["_id"] }, { :$set=>update })
179
+ # end
180
+ # [Server::AccessToken, Server::AccessGrant, Server::AuthRequest].each do |mod|
181
+ # puts "Updating #{mod.name} scope from string to array"
182
+ # mod.collection.find({ :scope=>{ :$type=>2 } }, :fields=>[]).each do |token|
183
+ # scope = token["scope"].split
184
+ # mod.collection.update({ :_id=>token["_id"] }, { :$set=>{ :scope=>scope } })
185
+ # end
186
+ # end
187
+ else
188
+
189
+ print <<-TEXT
190
+ Usage: oauth2-server [options] COMMAND [args]
191
+ Version #{Server::VERSION}
192
+
193
+ Commands:
194
+ list Lists all active clients
195
+ migrate Run this when migrating from 1.x to 2.x
196
+ practice Runs a dummy OAuth 2.0 server, use this to test your OAuth 2.0 client
197
+ register Register a new client application
198
+ setup Create new admin account and help you setup the OAuth Web console
199
+
200
+ Options:
201
+ --db database Database name or connection URL
202
+ --port number Port to run admin server, detault is 8080
203
+ TEXT
204
+ exit -1
205
+
206
+ end
@@ -0,0 +1,4 @@
1
+ require "rack/oauth2/server"
2
+ require "rack/oauth2/sinatra" if defined?(Sinatra)
3
+ require "rack/oauth2/rails" if defined?(Rails)
4
+ require "rack/oauth2/server/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,347 @@
1
+ html {
2
+ background: #eee;
3
+ }
4
+ body {
5
+ margin: 0;
6
+ padding: 0;
7
+ width: 100%;
8
+ font: 12pt "Helvetica", "Lucida Sans", "Verdana";
9
+ }
10
+
11
+ a { text-decoration: none; color: #00c; }
12
+ a:hover, a:focus { text-decoration: underline; color: #00c; }
13
+ h1, h2 {
14
+ text-shadow: rgba(255,255,255,.2) 0 1px 1px;
15
+ color: rgb(76, 86, 108);
16
+ }
17
+ h1 { font-size: 18pt; margin: 0.6em 0 }
18
+ h2 { font-size: 16pt; margin: 0.3em 0 }
19
+
20
+ label {
21
+ display: block;
22
+ color: #000;
23
+ font-weight: 600;
24
+ font-size: 0.9em;
25
+ margin: 0.9em 0;
26
+ }
27
+ label input, label textarea, label select {
28
+ display: block;
29
+ }
30
+ label.check {
31
+ font-weight: normal;
32
+ }
33
+ label.check input {
34
+ display: inline;
35
+ }
36
+ label input, label textarea {
37
+ font-size: 12pt;
38
+ line-height: 1.3em;
39
+ }
40
+ label .hint {
41
+ font-weight: normal;
42
+ color: #666;
43
+ margin: 0;
44
+ }
45
+ button {
46
+ font-size: 11pt;
47
+ text-shadow: 0 -1px 1px rgba(0,0,0,0.25);
48
+ border: 1px solid #dddddd;
49
+ background: #f6f6f6 50% 50% repeat-x;
50
+ font-weight: bold;
51
+ color: #0073ea;
52
+ outline: none;
53
+ line-height: 1.3em;
54
+ vertical-align: bottom;
55
+ padding: 2px 8px;
56
+ background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(226,226,226,0.0)), to(rgba(226,226,226,1.0)));
57
+ -webkit-border-radius: 4px; -moz-border-radius: 4px;
58
+ -moz-box-shadow: 0 0 4px rgba(0,0,0,0.0);
59
+ -webkit-box-shadow: 0 0 4px rgba(0,0,0,0.0);
60
+ }
61
+ button:hover, button:focus {
62
+ text-shadow: 0 -1px 1px rgba(255,255,255,0.25);
63
+ border: 1px solid #0073ea;
64
+ background: #0073ea 50% 50% repeat-x;
65
+ background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(0, 115, 234, 0.5)), to(rgba(0,115,234, 1.0)));
66
+ color: #fff;
67
+ text-decoration: none;
68
+ cursor: pointer;
69
+ -moz-box-shadow: 0 2px 4px rgba(0,0,0,0.5);
70
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.5);
71
+ }
72
+ button:active { background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(0, 115, 234, 1.0)), to(rgba(0,115,234, 0.5))); position: relative; top: 1px }
73
+ .error {
74
+ color: #f44;
75
+ padding-left: 1em;
76
+ }
77
+
78
+
79
+ /* Message dropping down from the top */
80
+ #notice {
81
+ position: absolute;
82
+ top: 0;
83
+ left: 0;
84
+ right: 0;
85
+ line-height: 1.6em;
86
+ background: #FFFE36;
87
+ color: #000;
88
+ border-bottom: 1px solid #ddd;
89
+ text-align: center;
90
+ z-index: 99;
91
+ }
92
+
93
+ #header {
94
+ margin: 0;
95
+ padding: 1em 2em 3em 2em;
96
+ border-bottom: 2px solid #CCC;
97
+ background: #6595A4;
98
+ color: #fff;
99
+ }
100
+ #header .title {
101
+ font-size: 18pt;
102
+ font-weight: bold;
103
+ display: block;
104
+ line-height: 32px;
105
+ float: left;
106
+ }
107
+ #header .title a {
108
+ color: #fff;
109
+ text-shadow: white 0px 0px 1px;
110
+ text-decoration: none;
111
+ }
112
+ #header .title img {
113
+ width: 32px;
114
+ height: 32px;
115
+ vertical-align: bottom;
116
+ }
117
+ #header .signout, #header .signin {
118
+ color: #C8E9F3;
119
+ float: right;
120
+ font-size: 11pt;
121
+ line-height: 32px;
122
+ display: none;
123
+ }
124
+
125
+ #main {
126
+ margin: 0;
127
+ padding: 2em 2em 4em 2em;
128
+ background: #fff;
129
+ border: 1px solid #fff;
130
+ min-width: 960px;
131
+ }
132
+ #footer {
133
+ background: #eee;
134
+ color: #666;
135
+ border-top: 1px solid #ccc;
136
+ font-size: 90%;
137
+ padding: 0 2em 2em 2em;
138
+ }
139
+
140
+ table {
141
+ width: 100%;
142
+ table-layout: auto;
143
+ empty-cells: show;
144
+ border-collapse: separate;
145
+ border-spacing: 0px;
146
+ margin-top: 2em;
147
+ }
148
+ table th {
149
+ text-align: left;
150
+ border-bottom: 1px solid #ccc;
151
+ margin-right: 48px;
152
+ }
153
+ table td {
154
+ text-align: left;
155
+ vertical-align: top;
156
+ border-bottom: 1px solid #ddf;
157
+ line-height: 32px;
158
+ margin: 0;
159
+ padding: 0;
160
+ }
161
+ table tr:hover td {
162
+ background: #ddf;
163
+ }
164
+ table td.created, table td.revoke, table td.accessed {
165
+ width: 6em;
166
+ }
167
+ table tr.revoked td, table tr.revoked a {
168
+ color: #888;
169
+ }
170
+ table button {
171
+ margin-top: -2px;
172
+ font-size: 10pt;
173
+ }
174
+
175
+ table.clients td.name {
176
+ padding-left: 32px;
177
+ }
178
+ table.clients td.name img {
179
+ width: 24px;
180
+ height: 24px;
181
+ border: none;
182
+ margin: 4px 4px -4px -32px;
183
+ }
184
+ table.clients td.secrets {
185
+ width: 28em;
186
+ }
187
+ table.clients td.secrets dl {
188
+ display: none;
189
+ width: 40em;
190
+ margin: 0 -12em 0.6em 0;
191
+ line-height: 1.3em;
192
+ }
193
+ table.clients td.secrets dt {
194
+ width: 4em;
195
+ float: left;
196
+ color: #888;
197
+ margin-bottom: 0.3em;
198
+ }
199
+ table.clients td.secrets dd:after {
200
+ content: ".";
201
+ display: block;
202
+ clear: both;
203
+ visibility: hidden;
204
+ line-height: 0;
205
+ height: 0;
206
+ }
207
+
208
+ table.tokens td.token {
209
+ width: 36em;
210
+ }
211
+ table.tokens td.scope {
212
+ float: none;
213
+ }
214
+
215
+ .pagination {
216
+ width: 100%;
217
+ margin-top: 2em;
218
+ }
219
+ .pagination a[rel=next] {
220
+ float: right;
221
+ }
222
+ .pagination a[rel=previous] {
223
+ float: left;
224
+ }
225
+
226
+ .metrics {
227
+ height: 100px;
228
+ }
229
+ .metrics #fig {
230
+ float: left;
231
+ width: 500px;
232
+ height: 60px;
233
+ }
234
+ .metrics:after {
235
+ content: ".";
236
+ display: block;
237
+ clear: both;
238
+ visibility: hidden;
239
+ line-height: 0;
240
+ height: 0;
241
+ }
242
+ .badges {
243
+ list-style: none;
244
+ margin: 0;
245
+ padding: 0;
246
+ text-align: right;
247
+ width: 100%;
248
+ }
249
+ .badges li {
250
+ display: inline-block;
251
+ margin-left: 8px;
252
+ min-width: 8em;
253
+ }
254
+ .badges big {
255
+ font-size: 22pt;
256
+ display: block;
257
+ text-align: center;
258
+ }
259
+ .badges small {
260
+ font-size: 11pt;
261
+ display: block;
262
+ text-align: center;
263
+ }
264
+
265
+ .client .details {
266
+ margin: 0 0 2em 0;
267
+ }
268
+ .client .details .name {
269
+ margin: 0;
270
+ font-size: 16pt;
271
+ font-weight: bold;
272
+ float: left;
273
+ }
274
+ .client .details img {
275
+ border: none;
276
+ width: 24px;
277
+ height: 24px;
278
+ vertical-align: bottom;
279
+ }
280
+ .client .details .actions {
281
+ float: left;
282
+ line-height: 20pt;
283
+ margin-left: 1em;
284
+ }
285
+ .client .details .actions a {
286
+ margin: 0 0 0 0.3em;
287
+ }
288
+ .client .details .meta {
289
+ clear: both;
290
+ display: block;
291
+ color: #888;
292
+ font-size: 10pt;
293
+ }
294
+ .client .details .notes {
295
+ font-size: 11pt;
296
+ margin: 0.2em 0;
297
+ }
298
+
299
+ .client.new, .client.edit {
300
+ margin: 0;
301
+ padding: 1em;
302
+ border: 1px solid #eee;
303
+ }
304
+ .client .fields>#image {
305
+ float: left;
306
+ margin: 0.5em 12px 0 0;
307
+ width: 48px;
308
+ height: 48px;
309
+ }
310
+ .client .fields>* {
311
+ margin-left: 60px;
312
+ }
313
+ .client .fields {
314
+ float: left;
315
+ }
316
+ .client .scope {
317
+ float: right;
318
+ }
319
+ .client .scope .uncommon {
320
+ color: red;
321
+ }
322
+ .client hr {
323
+ clear: both;
324
+ border: none;
325
+ margin: 1em;
326
+ }
327
+
328
+ .no-access {
329
+ margin: 0;
330
+ padding: 0;
331
+ }
332
+ .no-access h1 {
333
+ color: red;
334
+ }
335
+
336
+ #throbber {
337
+ display: none;
338
+ position: absolute;
339
+ top: 6em;
340
+ right: 2em;
341
+ width: 48px;
342
+ height: 48px;
343
+ content: "";
344
+ }
345
+ .loading {
346
+ background: url("../images/loading.gif") no-repeat 50% 50%;
347
+ }