mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 09:36:19 -05:00
Support for per-user API keys
This commit is contained in:
parent
5e2d8dcf37
commit
348e2e3ef2
45 changed files with 670 additions and 87 deletions
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
This controller supports the interface for dealing with API keys
|
||||
|
||||
@class AdminApiController
|
||||
@extends Ember.ArrayController
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.AdminApiController = Ember.ArrayController.extend({
|
||||
|
||||
actions: {
|
||||
/**
|
||||
Generates a master api key
|
||||
|
||||
@method generateMasterKey
|
||||
@param {Discourse.ApiKey} the key to regenerate
|
||||
**/
|
||||
generateMasterKey: function(key) {
|
||||
var self = this;
|
||||
Discourse.ApiKey.generateMasterKey().then(function (key) {
|
||||
self.get('model').pushObject(key);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Creates an API key instance with internal user object
|
||||
|
||||
@method regenerateKey
|
||||
@param {Discourse.ApiKey} the key to regenerate
|
||||
**/
|
||||
regenerateKey: function(key) {
|
||||
bootbox.confirm(I18n.t("admin.api.confirm_regen"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
|
||||
if (result) {
|
||||
key.regenerate();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Revokes an API key
|
||||
|
||||
@method revokeKey
|
||||
@param {Discourse.ApiKey} the key to revoke
|
||||
**/
|
||||
revokeKey: function(key) {
|
||||
var self = this;
|
||||
bootbox.confirm(I18n.t("admin.api.confirm_revoke"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
|
||||
if (result) {
|
||||
key.revoke().then(function() {
|
||||
self.get('model').removeObject(key);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
Has a master key already been generated?
|
||||
|
||||
@property hasMasterKey
|
||||
@type {Boolean}
|
||||
**/
|
||||
hasMasterKey: function() {
|
||||
return !!this.get('model').findBy('user', null);
|
||||
}.property('model.@each')
|
||||
|
||||
});
|
|
@ -27,6 +27,28 @@ Discourse.AdminUserController = Discourse.ObjectController.extend({
|
|||
});
|
||||
|
||||
this.send('toggleTitleEdit');
|
||||
},
|
||||
|
||||
generateApiKey: function() {
|
||||
this.get('model').generateApiKey();
|
||||
},
|
||||
|
||||
regenerateApiKey: function() {
|
||||
var self = this;
|
||||
bootbox.confirm(I18n.t("admin.api.confirm_regen"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
|
||||
if (result) {
|
||||
self.get('model').generateApiKey();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
revokeApiKey: function() {
|
||||
var self = this;
|
||||
bootbox.confirm(I18n.t("admin.api.confirm_revoke"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
|
||||
if (result) {
|
||||
self.get('model').revokeApiKey();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
Discourse.AdminApi = Discourse.Model.extend({
|
||||
VALID_KEY_LENGTH: 64,
|
||||
|
||||
keyExists: function(){
|
||||
var key = this.get('key') || '';
|
||||
return key && key.length === this.VALID_KEY_LENGTH;
|
||||
}.property('key'),
|
||||
|
||||
generateKey: function(){
|
||||
var adminApi = this;
|
||||
Discourse.ajax('/admin/api/generate_key', {type: 'POST'}).then(function (result) {
|
||||
adminApi.set('key', result.key);
|
||||
});
|
||||
},
|
||||
|
||||
regenerateKey: function(){
|
||||
alert(I18n.t('not_implemented'));
|
||||
}
|
||||
});
|
||||
|
||||
Discourse.AdminApi.reopenClass({
|
||||
find: function() {
|
||||
var model = Discourse.AdminApi.create();
|
||||
Discourse.ajax("/admin/api").then(function(data) {
|
||||
model.setProperties(data);
|
||||
});
|
||||
return model;
|
||||
}
|
||||
});
|
|
@ -8,6 +8,34 @@
|
|||
**/
|
||||
Discourse.AdminUser = Discourse.User.extend({
|
||||
|
||||
/**
|
||||
Generates an API key for the user. Will regenerate if they already have one.
|
||||
|
||||
@method generateApiKey
|
||||
@returns {Promise} a promise that resolves to the newly generated API key
|
||||
**/
|
||||
generateApiKey: function() {
|
||||
var self = this;
|
||||
return Discourse.ajax("/admin/users/" + this.get('id') + "/generate_api_key", {type: 'POST'}).then(function (result) {
|
||||
var apiKey = Discourse.ApiKey.create(result.api_key);
|
||||
self.set('api_key', apiKey);
|
||||
return apiKey;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Revokes a user's current API key
|
||||
|
||||
@method revokeApiKey
|
||||
@returns {Promise} a promise that resolves when the API key has been deleted
|
||||
**/
|
||||
revokeApiKey: function() {
|
||||
var self = this;
|
||||
return Discourse.ajax("/admin/users/" + this.get('id') + "/revoke_api_key", {type: 'DELETE'}).then(function (result) {
|
||||
self.set('api_key', null);
|
||||
});
|
||||
},
|
||||
|
||||
deleteAllPosts: function() {
|
||||
this.set('can_delete_all_posts', false);
|
||||
var user = this;
|
||||
|
|
81
app/assets/javascripts/admin/models/api_key.js
Normal file
81
app/assets/javascripts/admin/models/api_key.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
Our data model for representing an API key in the system
|
||||
|
||||
@class ApiKey
|
||||
@extends Discourse.Model
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.ApiKey = Discourse.Model.extend({
|
||||
|
||||
/**
|
||||
Regenerates the api key
|
||||
|
||||
@method regenerate
|
||||
@returns {Promise} a promise that resolves to the key
|
||||
**/
|
||||
regenerate: function() {
|
||||
var self = this;
|
||||
return Discourse.ajax('/admin/api/key', {type: 'PUT', data: {id: this.get('id')}}).then(function (result) {
|
||||
self.set('key', result.api_key.key);
|
||||
return self;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Revokes the current key
|
||||
|
||||
@method revoke
|
||||
@returns {Promise} a promise that resolves when the key has been revoked
|
||||
**/
|
||||
revoke: function() {
|
||||
var self = this;
|
||||
return Discourse.ajax('/admin/api/key', {type: 'DELETE', data: {id: this.get('id')}});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Discourse.ApiKey.reopenClass({
|
||||
|
||||
/**
|
||||
Creates an API key instance with internal user object
|
||||
|
||||
@method create
|
||||
@param {Object} the properties to create
|
||||
@returns {Discourse.ApiKey} the ApiKey instance
|
||||
**/
|
||||
create: function(apiKey) {
|
||||
var result = this._super(apiKey);
|
||||
if (result.user) {
|
||||
result.user = Discourse.AdminUser.create(result.user);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
Finds a list of API keys
|
||||
|
||||
@method find
|
||||
@returns {Promise} a promise that resolves to the array of `Discourse.ApiKey` instances
|
||||
**/
|
||||
find: function() {
|
||||
return Discourse.ajax("/admin/api").then(function(keys) {
|
||||
return keys.map(function (key) {
|
||||
return Discourse.ApiKey.create(key);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Generates a master api key and returns it.
|
||||
|
||||
@method generateMasterKey
|
||||
@returns {Promise} a promise that resolves to a master `Discourse.ApiKey`
|
||||
**/
|
||||
generateMasterKey: function() {
|
||||
return Discourse.ajax("/admin/api/key", {type: 'POST'}).then(function (result) {
|
||||
return Discourse.ApiKey.create(result.api_key);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
|
@ -9,7 +9,7 @@
|
|||
Discourse.AdminApiRoute = Discourse.Route.extend({
|
||||
|
||||
model: function() {
|
||||
return Discourse.AdminApi.find();
|
||||
return Discourse.ApiKey.find();
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -1,13 +1,33 @@
|
|||
<h3>{{i18n admin.api.long_title}}</h3>
|
||||
{{#if keyExists}}
|
||||
<strong>{{i18n admin.api.key}}:</strong> {{key}}
|
||||
<button class='btn' {{action regenerateKey target="model"}}>
|
||||
{{i18n admin.api.regenerate}}
|
||||
</button>
|
||||
<p>{{{i18n admin.api.note_html}}}</p>
|
||||
{{#if model}}
|
||||
<table class='api-keys'>
|
||||
<tr>
|
||||
<th>{{i18n admin.api.key}}</th>
|
||||
<th>{{i18n admin.api.user}}</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
{{#each model}}
|
||||
<tr>
|
||||
<td class='key'>{{key}}</td>
|
||||
<td>
|
||||
{{#if user}}
|
||||
{{#link-to 'adminUser' user}}
|
||||
{{avatar user imageSize="small"}}
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
{{i18n admin.api.all_users}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>
|
||||
<button class='btn' {{action regenerateKey this}}>{{i18n admin.api.regenerate}}</button>
|
||||
<button class='btn' {{action revokeKey this}}>{{i18n admin.api.revoke}}</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
{{else}}
|
||||
<p>{{{i18n admin.api.info_html}}}</p>
|
||||
<button class='btn' {{action generateKey target="model"}}>
|
||||
{{i18n admin.api.generate}}
|
||||
</button>
|
||||
<p>{{i18n admin.api.none}}</p>
|
||||
{{/if}}
|
||||
|
||||
{{#unless hasMasterKey}}
|
||||
<button class='btn' {{action generateMasterKey}}>{{i18n admin.api.generate_master}}</button>
|
||||
{{/unless }}
|
||||
|
|
|
@ -125,6 +125,25 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n admin.api.key}}</div>
|
||||
|
||||
{{#if api_key}}
|
||||
<div class='long-value'>
|
||||
{{api_key.key}}
|
||||
<button class='btn' {{action regenerateApiKey}}>{{i18n admin.api.regenerate}}</button>
|
||||
<button {{action revokeApiKey}} class="btn">{{i18n admin.api.revoke}}</button>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class='value'>
|
||||
—
|
||||
</div>
|
||||
<div class='controls'>
|
||||
<button {{action generateApiKey}} class="btn">{{i18n admin.api.generate}}</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n admin.user.admin}}</div>
|
||||
<div class='value'>{{admin}}</div>
|
||||
|
|
|
@ -206,6 +206,17 @@ table {
|
|||
float: left;
|
||||
margin-left: 12px;
|
||||
}
|
||||
.long-value {
|
||||
width: 800px;
|
||||
float: left;
|
||||
margin-left: 12px;
|
||||
font-size: 13px;
|
||||
|
||||
button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
.btn {
|
||||
margin-right: 5px;
|
||||
|
@ -374,6 +385,24 @@ table {
|
|||
}
|
||||
}
|
||||
|
||||
table.api-keys {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
td.key {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-stats {
|
||||
margin-bottom: 30px;
|
||||
width: 460px;
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
class Admin::ApiController < Admin::AdminController
|
||||
|
||||
def index
|
||||
render json: {key: SiteSetting.api_key}
|
||||
render_serialized(ApiKey.all, ApiKeySerializer)
|
||||
end
|
||||
|
||||
def generate_key
|
||||
SiteSetting.generate_api_key!
|
||||
render json: {key: SiteSetting.api_key}
|
||||
def regenerate_key
|
||||
api_key = ApiKey.where(id: params[:id]).first
|
||||
raise Discourse::NotFound.new if api_key.blank?
|
||||
|
||||
api_key.regenerate!(current_user)
|
||||
render_serialized(api_key, ApiKeySerializer)
|
||||
end
|
||||
|
||||
def revoke_key
|
||||
api_key = ApiKey.where(id: params[:id]).first
|
||||
raise Discourse::NotFound.new if api_key.blank?
|
||||
|
||||
api_key.destroy
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
def create_master_key
|
||||
api_key = ApiKey.create_master_key
|
||||
render_serialized(api_key, ApiKeySerializer)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -4,7 +4,21 @@ require_dependency 'boost_trust_level'
|
|||
|
||||
class Admin::UsersController < Admin::AdminController
|
||||
|
||||
before_filter :fetch_user, only: [:ban, :unban, :refresh_browsers, :revoke_admin, :grant_admin, :revoke_moderation, :grant_moderation, :approve, :activate, :deactivate, :block, :unblock, :trust_level]
|
||||
before_filter :fetch_user, only: [:ban,
|
||||
:unban,
|
||||
:refresh_browsers,
|
||||
:revoke_admin,
|
||||
:grant_admin,
|
||||
:revoke_moderation,
|
||||
:grant_moderation,
|
||||
:approve,
|
||||
:activate,
|
||||
:deactivate,
|
||||
:block,
|
||||
:unblock,
|
||||
:trust_level,
|
||||
:generate_api_key,
|
||||
:revoke_api_key]
|
||||
|
||||
def index
|
||||
query = ::AdminUserIndexQuery.new(params)
|
||||
|
@ -52,6 +66,16 @@ class Admin::UsersController < Admin::AdminController
|
|||
render nothing: true
|
||||
end
|
||||
|
||||
def generate_api_key
|
||||
api_key = @user.generate_api_key(current_user)
|
||||
render_serialized(api_key, ApiKeySerializer)
|
||||
end
|
||||
|
||||
def revoke_api_key
|
||||
@user.revoke_api_key
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
def grant_admin
|
||||
guardian.ensure_can_grant_admin!(@user)
|
||||
@user.grant_admin!
|
||||
|
|
|
@ -279,7 +279,7 @@ class ApplicationController < ActionController::Base
|
|||
protected
|
||||
|
||||
def api_key_valid?
|
||||
request["api_key"] && SiteSetting.api_key_valid?(request["api_key"])
|
||||
request["api_key"] && ApiKey.where(key: request["api_key"]).exists?
|
||||
end
|
||||
|
||||
# returns an array of integers given a param key
|
||||
|
|
22
app/models/api_key.rb
Normal file
22
app/models/api_key.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
class ApiKey < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :created_by, class_name: User
|
||||
|
||||
validates_presence_of :key
|
||||
validates_uniqueness_of :user_id
|
||||
|
||||
def regenerate!(updated_by)
|
||||
self.key = SecureRandom.hex(32)
|
||||
self.created_by = updated_by
|
||||
save!
|
||||
end
|
||||
|
||||
def self.create_master_key
|
||||
api_key = ApiKey.where('user_id IS NULL').first
|
||||
if api_key.blank?
|
||||
api_key = ApiKey.create(key: SecureRandom.hex(32), created_by: Discourse.system_user)
|
||||
end
|
||||
api_key
|
||||
end
|
||||
|
||||
end
|
|
@ -18,7 +18,6 @@ class SiteSetting < ActiveRecord::Base
|
|||
client_setting(:tos_url, '')
|
||||
client_setting(:faq_url, '')
|
||||
client_setting(:privacy_policy_url, '')
|
||||
setting(:api_key, '')
|
||||
client_setting(:traditional_markdown_linebreaks, false)
|
||||
client_setting(:top_menu, 'latest|new|unread|favorited|categories')
|
||||
client_setting(:post_menu, 'like|edit|flag|delete|share|bookmark|reply')
|
||||
|
@ -273,15 +272,6 @@ class SiteSetting < ActiveRecord::Base
|
|||
|
||||
setting(:dominating_topic_minimum_percent, 20)
|
||||
|
||||
def self.generate_api_key!
|
||||
self.api_key = SecureRandom.hex(32)
|
||||
end
|
||||
|
||||
def self.api_key_valid?(tested)
|
||||
t = tested.strip
|
||||
t.length == 64 && t == self.api_key
|
||||
end
|
||||
|
||||
def self.call_discourse_hub?
|
||||
self.enforce_global_nicknames? && self.discourse_org_access_key.present?
|
||||
end
|
||||
|
|
|
@ -43,6 +43,7 @@ class User < ActiveRecord::Base
|
|||
has_many :secure_categories, through: :groups, source: :categories
|
||||
|
||||
has_one :user_search_data, dependent: :destroy
|
||||
has_one :api_key, dependent: :destroy
|
||||
|
||||
belongs_to :uploaded_avatar, class_name: 'Upload', dependent: :destroy
|
||||
|
||||
|
@ -479,6 +480,19 @@ class User < ActiveRecord::Base
|
|||
self.save!
|
||||
end
|
||||
|
||||
def generate_api_key(created_by)
|
||||
if api_key.present?
|
||||
api_key.regenerate!(created_by)
|
||||
api_key
|
||||
else
|
||||
ApiKey.create(user: self, key: SecureRandom.hex(32), created_by: created_by)
|
||||
end
|
||||
end
|
||||
|
||||
def revoke_api_key
|
||||
ApiKey.where(user_id: self.id).delete_all
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def cook
|
||||
|
@ -567,6 +581,7 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
end
|
||||
|
|
|
@ -16,6 +16,7 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
|||
:can_be_deleted
|
||||
|
||||
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
|
||||
has_one :api_key, serializer: ApiKeySerializer, embed: :objects
|
||||
|
||||
def can_revoke_admin
|
||||
scope.can_revoke_admin?(object)
|
||||
|
@ -49,4 +50,8 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
|||
object.topics.count
|
||||
end
|
||||
|
||||
def include_api_key?
|
||||
api_key.present?
|
||||
end
|
||||
|
||||
end
|
||||
|
|
12
app/serializers/api_key_serializer.rb
Normal file
12
app/serializers/api_key_serializer.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
class ApiKeySerializer < ApplicationSerializer
|
||||
|
||||
attributes :id,
|
||||
:key
|
||||
|
||||
has_one :user, serializer: BasicUserSerializer, embed: :objects
|
||||
|
||||
def include_user_id?
|
||||
!object.user_id.nil?
|
||||
end
|
||||
|
||||
end
|
|
@ -1169,12 +1169,18 @@ en:
|
|||
delete_failed: "Unable to delete group. If this is an automatic group, it cannot be destroyed."
|
||||
|
||||
api:
|
||||
generate_master: "Generate Master API Key"
|
||||
none: "There are no active API keys right now."
|
||||
user: "User"
|
||||
title: "API"
|
||||
long_title: "API Information"
|
||||
key: "Key"
|
||||
generate: "Generate API Key"
|
||||
regenerate: "Regenerate API Key"
|
||||
key: "API Key"
|
||||
generate: "Generate"
|
||||
regenerate: "Regenerate"
|
||||
revoke: "Revoke"
|
||||
confirm_regen: "Are you sure you want to replace that API Key with a new one?"
|
||||
confirm_revoke: "Are you sure you want to revoke that key?"
|
||||
info_html: "Your API key will allow you to create and update topics using JSON calls."
|
||||
all_users: "All Users"
|
||||
note_html: "Keep this key <strong>secret</strong>, all users that have it may create arbitrary posts on the forum as any user."
|
||||
|
||||
customize:
|
||||
|
|
|
@ -493,7 +493,6 @@ cs:
|
|||
company_full_name: "Plné jméno společnosti, která provozuje tento web, používá se v dokumentech jako je /tos"
|
||||
company_short_name: "Krátké jméno společnosti, která provozuje tento web, používá se v dokumentech jako je /tos"
|
||||
company_domain: "Doménové jméno vlastněné společností, která provozuje tento web, používá se v dokumentech jako je /tos"
|
||||
api_key: "Zabezpečený API klíč, který se používá pro vytváření a aktualizaci témat, použijte sekci /admin/api k nastavení"
|
||||
queue_jobs: "Zařazovat úlohy do fronty v sidekiq, není-li nastaveno, jsou úlohy vyřizovány okamžitě"
|
||||
crawl_images: "Povolit získávání obrázků z webů třetích stran"
|
||||
ninja_edit_window: "Jak rychle smíte udělat změnu, aniž by se uložila jako nová verze"
|
||||
|
|
|
@ -463,7 +463,6 @@ de:
|
|||
company_full_name: "Voller Name des Unternehmens, das diese Seite betreibt. Wird in rechtlich relevanten Dokumenten wie den Nutzungsbestimmungen (/tos) verwendet."
|
||||
company_short_name: "Kurzname des Unternehmens, das diese Seite betreibt. Wird in rechtlich relevanten Dokumenten wie den Nutzungsbestimmungen (/tos) verwendet."
|
||||
company_domain: "Domainname des Unternehmens, das diese Seite betreibt. Wird in rechtlich relevanten Dokumenten wie den Nutzungsbestimmungen (/tos) verwendet."
|
||||
api_key: "Sicherer API-Schlüssel, um Themen zu erstellen und zu aktualisieren. Benutze /admin/api, um ihn einzurichten."
|
||||
queue_jobs: "Benutze die Sidekiq-Queue, falls falsche Queues inline sind."
|
||||
crawl_images: "Lade Bilder von Dritten herunter, um ihre Höhe und Breite zu bestimmen."
|
||||
ninja_edit_window: "Sekunden nach Empfang eines Beitrag, in denen Bearbeitungen nicht als neue Version gelten."
|
||||
|
|
|
@ -495,7 +495,6 @@ en:
|
|||
company_full_name: "The full name of the company that runs this site, used in legal documents like the /tos"
|
||||
company_short_name: "The short name of the company that runs this site, used in legal documents like the /tos"
|
||||
company_domain: "The domain name owned by the company that runs this site, used in legal documents like the /tos"
|
||||
api_key: "The secure API key used to create and update topics, use the /admin/api section to set it up"
|
||||
queue_jobs: "DEVELOPER ONLY! WARNING! By default, queue jobs in sidekiq. If disabled, your site will be broken."
|
||||
crawl_images: "Enable retrieving images from third party sources to insert width and height dimensions"
|
||||
ninja_edit_window: "Number of seconds after posting where edits do not create a new version"
|
||||
|
|
|
@ -460,7 +460,6 @@ fr:
|
|||
company_full_name: "Le nom complet de la société qui gère ce site, utilisé dans les documents légaux, tels que /tos"
|
||||
company_short_name: "Le nom de la société qui gère ce site, utilisé dans les documents légaux, tels que /tos"
|
||||
company_domain: "Le nom de domaine de la société qui gère ce site, utilisé dans les documents légaux, tels que /tos"
|
||||
api_key: "La clé API sécurisé à utiliser pour créer et mettre à jour des discussions. Utilisez la section /admin/api pour la configurer."
|
||||
queue_jobs: "DÉVELOPPEURS SEULEMENT ! ATTENTION ! Par défaut, empiler les travaux dans sidekiq. Si désactivé, votre site sera cassé."
|
||||
crawl_images: "Permettre aux images provenant de sources tierces d'insérer la hauteur et la largeur de celles-ci"
|
||||
ninja_edit_window: "Temps d'édition avant de sauvegarder une nouvelle version, en secondes."
|
||||
|
|
|
@ -423,7 +423,6 @@ it:
|
|||
company_full_name: "Il nome completo di chi gestisce il sito, usato in documenti legali come /tos"
|
||||
company_short_name: "Il nome abbreviato di chi gestisce il sito, usato in documenti legali come /tos"
|
||||
company_domain: "Il dominio di chi gestisce il sito, usato in documenti legali come /tos"
|
||||
api_key: "La chiave API segreta usata per creare e aggiornare topic, usa la sezione /admin/api per impostarla"
|
||||
queue_jobs: "Metti in coda diversi job in sidekiq, se false le code sono inline"
|
||||
crawl_images: "Abilita la ricezione di immagini da sorgenti terze parti"
|
||||
ninja_edit_window: "Numero di secondi trascorsi affinché una modifica del post appena inviato, non venga considerata come nuova revisione"
|
||||
|
|
|
@ -417,7 +417,6 @@ ko:
|
|||
company_full_name: "The full name of the company that runs this site, used in legal documents like the /tos"
|
||||
company_short_name: "The short name of the company that runs this site, used in legal documents like the /tos"
|
||||
company_domain: "The domain name owned by the company that runs this site, used in legal documents like the /tos"
|
||||
api_key: "The secure API key used to create and update topics, use the /admin/api section to set it up"
|
||||
access_password: "When restricted access is enabled, this password must be entered"
|
||||
queue_jobs: "Queue various jobs in sidekiq, if false queues are inline"
|
||||
crawl_images: "Enable retrieving images from third party sources to insert width and height dimensions"
|
||||
|
|
|
@ -465,7 +465,6 @@ nl:
|
|||
company_full_name: "De volledige naam van het bedrijf dat deze site draait. Wordt gebruikt in juridische delen van de site, zoals /tos"
|
||||
company_short_name: "De korte naam van het bedrijf dat deze site draait. Wordt gebruikt in juridische delen van de site, zoals /tos"
|
||||
company_domain: "De domeinnaam van het bedrijf dat deze site draait. Wordt gebruikt in juridische delen van de site, zoals /tos"
|
||||
api_key: "De beveiligde API-sleutel wordt gebruikt om topics te maken en bij te werken. Gebruik /admin/api om deze in te stellen"
|
||||
queue_jobs: "DEVELOPERS ONLY! WARNING! Zet verschillende taken in een queue binnen sidekiq, bij 'false' worden taken ineens uitgevoerd"
|
||||
crawl_images: Zet het ophalen van afbeeldingen van externe bronnen aan
|
||||
ninja_edit_window: "Hoe snel je een aanpassing kan maken zonder dat er een nieuwe versie wordt opgeslagen, in seconden."
|
||||
|
|
|
@ -543,8 +543,6 @@ pseudo:
|
|||
íɳ łéǧáł ďóčůɱéɳťš łíǩé ťĥé /ťóš ]]'
|
||||
company_domain: '[[ Ťĥé ďóɱáíɳ ɳáɱé óŵɳéď ƀý ťĥé čóɱƿáɳý ťĥáť řůɳš ťĥíš šíťé,
|
||||
ůšéď íɳ łéǧáł ďóčůɱéɳťš łíǩé ťĥé /ťóš ]]'
|
||||
api_key: '[[ Ťĥé šéčůřé ÁРÍ ǩéý ůšéď ťó čřéáťé áɳď ůƿďáťé ťóƿíčš, ůšé ťĥé /áďɱíɳ/áƿí
|
||||
šéčťíóɳ ťó šéť íť ůƿ ]]'
|
||||
queue_jobs: '[[ Ƣůéůé νáříóůš ʲóƀš íɳ šíďéǩíƣ, íƒ ƒáłšé ƣůéůéš ářé íɳłíɳé ]]'
|
||||
crawl_images: '[[ Éɳáƀłé řéťříéνíɳǧ íɱáǧéš ƒřóɱ ťĥířď ƿářťý šóůřčéš ťó íɳšéřť
|
||||
ŵíďťĥ áɳď ĥéíǧĥť ďíɱéɳšíóɳš ]]'
|
||||
|
|
|
@ -489,7 +489,6 @@ pt_BR:
|
|||
company_full_name: "Nome completo da companhia que mantém este site, usada nos documentos legais como o /tos"
|
||||
company_short_name: "Nome curto da companhia que mantém este site, usada nos documentos legais como o /tos"
|
||||
company_domain: "Nome de domínio pertencente a companhia que mantém este site, usada nos documentos legais como o /tos"
|
||||
api_key: "A chave de API segura usada para criar e modificar tópicos, acesse a seção /admin/api para defini-lá"
|
||||
queue_jobs: "APENAS DESENVOLVEDORES! ATENÇÃO! Por padrão, enfileira tarefas no sidekiq. Se desativado, seu site ficará defeituoso."
|
||||
crawl_images: "permitir mostrar imagens de sites terceiros"
|
||||
ninja_edit_window: "quão rápido é possivél fazer uma alteração sem guardar uma nova versão, em segundos."
|
||||
|
|
|
@ -484,7 +484,6 @@ ru:
|
|||
company_full_name: 'Полное название компании, которой принадлежит сайт, используется в правовой документации как /tos'
|
||||
company_short_name: 'Короткое название компании, которой принадлежит сайт, используется в правовой документации как /tos'
|
||||
company_domain: 'Имя домена, принадлежащего компании, заведующей сайтом, используется в правовой документации как /tos'
|
||||
api_key: 'Секретный API ключ, используемый для создания и обновления тем. Зайдите в секцию /admin/api , чтобы его задать'
|
||||
queue_jobs: 'ТОЛЬКО ДЛЯ РАЗРАБОТЧИКОВ! ВНИМАНИЕ! По умолчанию задачи обрабатываются асинхронно в очереди sidekiq. Если настройка выключена, ваш сайт может не работать.'
|
||||
crawl_images: 'Разрешить извлечение изображений из сторонних источников, ширина и высота'
|
||||
ninja_edit_window: 'Количество секунд после размещения сообщения, в течение которых внесение правок в сообщение не повлечет его изменение'
|
||||
|
|
|
@ -346,7 +346,6 @@ sv:
|
|||
company_full_name: "Det fullständiga namnet för företaget som driver denna webbplats, används i juridiska dokument så som /tos"
|
||||
company_short_name: "Det korta namnet för företaget som driver denna webbplats, används i juridiska dokument så som /tos"
|
||||
company_domain: "Domännamnet som ägs av företaget som driver denna webbplats, används i juridiska dokument så som /tos"
|
||||
api_key: "Den säkra API-nyckeln som används för att skapa och uppdatera trådar, använd /admin/api för att skapa en"
|
||||
queue_jobs: "Köa diverse jobb i sidekiq, om urkryssat så körs köer infogat"
|
||||
crawl_images: "Aktivera hämtning av bilder från tredjepartskällor för att infoga bredd och höjd"
|
||||
ninja_edit_window: "Antal sekunder efter ett inlägg när en ändring inte skapar en ny version"
|
||||
|
|
|
@ -119,8 +119,8 @@ zh_CN:
|
|||
replace_paragraph: "[用一段简短的分类描述替换此第一段内容,请不要超过200个字符。]"
|
||||
post_template: "%{replace_paragraph}\n\n使用下面的空间输入分类的详细描述信息,可包括在此分类下讨论的规则、内容导向等等。"
|
||||
this_year: "今年"
|
||||
|
||||
|
||||
|
||||
|
||||
trust_levels:
|
||||
newuser:
|
||||
title: "访客"
|
||||
|
@ -442,7 +442,6 @@ zh_CN:
|
|||
company_full_name: "运行本站点的公司全称,用于法律文档,例如服务条款 /tos"
|
||||
company_short_name: "运行本站点的公司短名,用于法律文档,例如服务条款 /tos"
|
||||
company_domain: "运行本站点的公司域名,用于法律文档,例如服务条款 /tos"
|
||||
api_key: "加密的应用开发接口密钥(API key),用于创建和更新主题。使用 /admin/api 来对它进行设置。"
|
||||
queue_jobs: "如果失败队列在排队,使用 Sidekiq 消息引擎对不同的工作排队"
|
||||
crawl_images: "允许从第三方获取图片来插入宽、高数值"
|
||||
ninja_edit_window: "在多少秒钟之内,对帖子的多次编辑不生成新版本"
|
||||
|
|
|
@ -423,7 +423,6 @@ zh_TW:
|
|||
company_full_name: "運行本站點的公司全稱,用于法律文檔,例如服務條款 /tos"
|
||||
company_short_name: "運行本站點的公司短名,用于法律文檔,例如服務條款 /tos"
|
||||
company_domain: "運行本站點的公司域名,用于法律文檔,例如服務條款 /tos"
|
||||
api_key: "加密的應用開發接口密鑰(API key),用于創建和更新主題。使用 /admin/api 來對它進行設置。"
|
||||
queue_jobs: "如果失敗隊列在排隊,使用 Sidekiq 消息引擎對不同的工作排隊"
|
||||
crawl_images: "允許從第三方獲取圖片來插入寬、高數值"
|
||||
ninja_edit_window: "在多少秒鍾之內,對帖子的多次編輯不生成新版本"
|
||||
|
|
|
@ -43,6 +43,8 @@ Discourse::Application.routes.draw do
|
|||
put 'unban'
|
||||
put 'revoke_admin', constraints: AdminConstraint.new
|
||||
put 'grant_admin', constraints: AdminConstraint.new
|
||||
post 'generate_api_key', constraints: AdminConstraint.new
|
||||
delete 'revoke_api_key', constraints: AdminConstraint.new
|
||||
put 'revoke_moderation', constraints: AdminConstraint.new
|
||||
put 'grant_moderation', constraints: AdminConstraint.new
|
||||
put 'approve'
|
||||
|
@ -89,7 +91,9 @@ Discourse::Application.routes.draw do
|
|||
end
|
||||
resources :api, only: [:index], constraints: AdminConstraint.new do
|
||||
collection do
|
||||
post 'generate_key'
|
||||
post 'key' => 'api#create_master_key'
|
||||
put 'key' => 'api#regenerate_key'
|
||||
delete 'key' => 'api#revoke_key'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
16
db/migrate/20131022151218_create_api_keys.rb
Normal file
16
db/migrate/20131022151218_create_api_keys.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class CreateApiKeys < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :api_keys do |t|
|
||||
t.string :key, limit: 64, null: false
|
||||
t.integer :user_id, null: true
|
||||
t.integer :created_by_id
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :api_keys, :key
|
||||
add_index :api_keys, :user_id, unique: true
|
||||
|
||||
execute "INSERT INTO api_keys (key, created_at, updated_at) SELECT value, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP FROM site_settings WHERE name = 'api_key'"
|
||||
execute "DELETE FROM site_settings WHERE name = 'api_key'"
|
||||
end
|
||||
end
|
|
@ -38,12 +38,17 @@ class Auth::DefaultCurrentUserProvider
|
|||
|
||||
# possible we have an api call, impersonate
|
||||
unless current_user
|
||||
if api_key = request["api_key"]
|
||||
if api_username = request["api_username"]
|
||||
if SiteSetting.api_key_valid?(api_key)
|
||||
@env[API_KEY] = true
|
||||
if api_key_value = request["api_key"]
|
||||
api_key = ApiKey.where(key: api_key_value).includes(:user).first
|
||||
if api_key.present?
|
||||
@env[API_KEY] = true
|
||||
|
||||
if api_key.user.present?
|
||||
current_user = api_key.user
|
||||
elsif api_username = request["api_username"]
|
||||
current_user = User.where(username_lower: api_username.downcase).first
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
desc "generate api key if missing, return existing if already there"
|
||||
task "api_key:get" => :environment do
|
||||
if SiteSetting.api_key.blank?
|
||||
SiteSetting.generate_api_key!
|
||||
end
|
||||
api_key = ApiKey.create_master_key
|
||||
|
||||
puts SiteSetting.api_key
|
||||
puts api_key.key
|
||||
end
|
||||
|
|
56
spec/controllers/admin/api_controller_spec.rb
Normal file
56
spec/controllers/admin/api_controller_spec.rb
Normal file
|
@ -0,0 +1,56 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Admin::ApiController do
|
||||
|
||||
it "is a subclass of AdminController" do
|
||||
(Admin::ApiController < Admin::AdminController).should be_true
|
||||
end
|
||||
|
||||
let!(:user) { log_in(:admin) }
|
||||
|
||||
context '.index' do
|
||||
it "succeeds" do
|
||||
xhr :get, :index
|
||||
response.should be_success
|
||||
end
|
||||
end
|
||||
|
||||
context '.regenerate_key' do
|
||||
let(:api_key) { Fabricate(:api_key) }
|
||||
|
||||
it "returns 404 when there is no key" do
|
||||
xhr :put, :regenerate_key, id: 1234
|
||||
response.should_not be_success
|
||||
response.status.should == 404
|
||||
end
|
||||
|
||||
it "delegates to the api key's `regenerate!` method" do
|
||||
ApiKey.any_instance.expects(:regenerate!)
|
||||
xhr :put, :regenerate_key, id: api_key.id
|
||||
end
|
||||
end
|
||||
|
||||
context '.revoke_key' do
|
||||
let(:api_key) { Fabricate(:api_key) }
|
||||
|
||||
it "returns 404 when there is no key" do
|
||||
xhr :delete, :revoke_key, id: 1234
|
||||
response.should_not be_success
|
||||
response.status.should == 404
|
||||
end
|
||||
|
||||
it "delegates to the api key's `regenerate!` method" do
|
||||
ApiKey.any_instance.expects(:destroy)
|
||||
xhr :delete, :revoke_key, id: api_key.id
|
||||
end
|
||||
end
|
||||
|
||||
context '.create_master_key' do
|
||||
it "creates a record" do
|
||||
lambda {
|
||||
xhr :post, :create_master_key
|
||||
}.should change(ApiKey, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -62,6 +62,26 @@ describe Admin::UsersController do
|
|||
|
||||
end
|
||||
|
||||
context '.generate_api_key' do
|
||||
let(:evil_trout) { Fabricate(:evil_trout) }
|
||||
|
||||
it 'calls generate_api_key' do
|
||||
User.any_instance.expects(:generate_api_key).with(@user)
|
||||
xhr :post, :generate_api_key, user_id: evil_trout.id
|
||||
end
|
||||
end
|
||||
|
||||
context '.revoke_api_key' do
|
||||
|
||||
let(:evil_trout) { Fabricate(:evil_trout) }
|
||||
|
||||
it 'calls revoke_api_key' do
|
||||
User.any_instance.expects(:revoke_api_key)
|
||||
xhr :delete, :revoke_api_key, user_id: evil_trout.id
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context '.approve' do
|
||||
|
||||
let(:evil_trout) { Fabricate(:evil_trout) }
|
||||
|
|
|
@ -15,10 +15,18 @@ describe 'api' do
|
|||
Fabricate(:post)
|
||||
end
|
||||
|
||||
let(:api_key) { user.generate_api_key(user) }
|
||||
let(:master_key) { ApiKey.create_master_key }
|
||||
|
||||
# choosing an arbitrarily easy to mock trusted activity
|
||||
it 'allows users with api key to bookmark posts' do
|
||||
PostAction.expects(:act).with(user, post, PostActionType.types[:bookmark]).once
|
||||
put :bookmark, bookmarked: "true", post_id: post.id, api_key: SiteSetting.api_key, api_username: user.username, format: :json
|
||||
put :bookmark, bookmarked: "true", post_id: post.id, api_key: api_key.key, format: :json
|
||||
end
|
||||
|
||||
it 'allows users with a master api key to bookmark posts' do
|
||||
PostAction.expects(:act).with(user, post, PostActionType.types[:bookmark]).once
|
||||
put :bookmark, bookmarked: "true", post_id: post.id, api_key: master_key.key, api_username: user.username, format: :json
|
||||
end
|
||||
|
||||
it 'disallows phonies to bookmark posts' do
|
||||
|
|
3
spec/fabricators/api_key_fabricator.rb
Normal file
3
spec/fabricators/api_key_fabricator.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
Fabricator(:api_key) do
|
||||
key '1dfb7d427400cb8ef18052fd412781af134cceca5725dd74f34bbc6b9e35ddc9'
|
||||
end
|
16
spec/models/api_key_spec.rb
Normal file
16
spec/models/api_key_spec.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# encoding: utf-8
|
||||
require 'spec_helper'
|
||||
require_dependency 'api_key'
|
||||
|
||||
describe ApiKey do
|
||||
it { should belong_to :user }
|
||||
it { should belong_to :created_by }
|
||||
|
||||
it { should validate_presence_of :key }
|
||||
|
||||
it 'validates uniqueness of user_id' do
|
||||
Fabricate(:api_key)
|
||||
should validate_uniqueness_of(:user_id)
|
||||
end
|
||||
|
||||
end
|
|
@ -863,4 +863,55 @@ describe User do
|
|||
expect(user.update_avatar(upload)).to be_true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'api keys' do
|
||||
let(:admin) { Fabricate(:admin) }
|
||||
let(:other_admin) { Fabricate(:admin) }
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
describe '.generate_api_key' do
|
||||
|
||||
it "generates an api key when none exists, and regenerates when it does" do
|
||||
expect(user.api_key).to be_blank
|
||||
|
||||
# Generate a key
|
||||
api_key = user.generate_api_key(admin)
|
||||
expect(api_key.user).to eq(user)
|
||||
expect(api_key.key).to be_present
|
||||
expect(api_key.created_by).to eq(admin)
|
||||
|
||||
user.reload
|
||||
expect(user.api_key).to eq(api_key)
|
||||
|
||||
# Regenerate a key. Keeps the same record, updates the key
|
||||
new_key = user.generate_api_key(other_admin)
|
||||
expect(new_key.id).to eq(api_key.id)
|
||||
expect(new_key.key).to_not eq(api_key.key)
|
||||
expect(new_key.created_by).to eq(other_admin)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe '.revoke_api_key' do
|
||||
|
||||
it "revokes an api key when exists" do
|
||||
expect(user.api_key).to be_blank
|
||||
|
||||
# Revoke nothing does nothing
|
||||
user.revoke_api_key
|
||||
user.reload
|
||||
expect(user.api_key).to be_blank
|
||||
|
||||
# When a key is present it is removed
|
||||
user.generate_api_key(admin)
|
||||
user.reload
|
||||
user.revoke_api_key
|
||||
user.reload
|
||||
expect(user.api_key).to be_blank
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
9
test/fixtures/api_keys.yml
vendored
Normal file
9
test/fixtures/api_keys.yml
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
|
||||
|
||||
one:
|
||||
key: MyString
|
||||
user_id: 1
|
||||
|
||||
two:
|
||||
key: MyString
|
||||
user_id: 1
|
30
test/javascripts/admin/models/admin_user_test.js
Normal file
30
test/javascripts/admin/models/admin_user_test.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
module("Discourse.AdminUser");
|
||||
|
||||
|
||||
asyncTestDiscourse('generate key', function() {
|
||||
this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve({api_key: {id: 1234, key: 'asdfasdf'}}));
|
||||
|
||||
var adminUser = Discourse.AdminUser.create({id: 333});
|
||||
|
||||
blank(adminUser.get('api_key'), 'it has no api key by default');
|
||||
adminUser.generateApiKey().then(function() {
|
||||
start();
|
||||
ok(Discourse.ajax.calledWith("/admin/users/333/generate_api_key", { type: 'POST' }), "it POSTed to the url");
|
||||
present(adminUser.get('api_key'), 'it has an api_key now');
|
||||
});
|
||||
});
|
||||
|
||||
asyncTestDiscourse('revoke key', function() {
|
||||
|
||||
var apiKey = Discourse.ApiKey.create({id: 1234, key: 'asdfasdf'}),
|
||||
adminUser = Discourse.AdminUser.create({id: 333, api_key: apiKey});
|
||||
|
||||
this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve());
|
||||
|
||||
equal(adminUser.get('api_key'), apiKey, 'it has the api key in the beginning');
|
||||
adminUser.revokeApiKey().then(function() {
|
||||
start();
|
||||
ok(Discourse.ajax.calledWith("/admin/users/333/revoke_api_key", { type: 'DELETE' }), "it DELETEd to the url");
|
||||
blank(adminUser.get('api_key'), 'it cleared the api_key');
|
||||
});
|
||||
});
|
45
test/javascripts/admin/models/api_key_test.js
Normal file
45
test/javascripts/admin/models/api_key_test.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
module("Discourse.ApiKey");
|
||||
|
||||
test('create', function() {
|
||||
var apiKey = Discourse.ApiKey.create({id: 123, user: {id: 345}});
|
||||
|
||||
present(apiKey, 'it creates the api key');
|
||||
present(apiKey.get('user'), 'it creates the user inside');
|
||||
});
|
||||
|
||||
|
||||
asyncTestDiscourse('find', function() {
|
||||
this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve([]));
|
||||
Discourse.ApiKey.find().then(function() {
|
||||
start();
|
||||
ok(Discourse.ajax.calledWith("/admin/api"), "it GETs the keys");
|
||||
});
|
||||
});
|
||||
|
||||
asyncTestDiscourse('generateMasterKey', function() {
|
||||
this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve([]));
|
||||
Discourse.ApiKey.generateMasterKey().then(function() {
|
||||
start();
|
||||
ok(Discourse.ajax.calledWith("/admin/api/key", {type: 'POST'}), "it POSTs to create a master key");
|
||||
});
|
||||
});
|
||||
|
||||
asyncTestDiscourse('regenerate', function() {
|
||||
var apiKey = Discourse.ApiKey.create({id: 3456});
|
||||
|
||||
this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve({api_key: {id: 3456}}));
|
||||
apiKey.regenerate().then(function() {
|
||||
start();
|
||||
ok(Discourse.ajax.calledWith("/admin/api/key", {type: 'PUT', data: {id: 3456}}), "it PUTs the key");
|
||||
});
|
||||
});
|
||||
|
||||
asyncTestDiscourse('revoke', function() {
|
||||
var apiKey = Discourse.ApiKey.create({id: 3456});
|
||||
|
||||
this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve([]));
|
||||
apiKey.revoke().then(function() {
|
||||
start();
|
||||
ok(Discourse.ajax.calledWith("/admin/api/key", {type: 'DELETE', data: {id: 3456}}), "it DELETES the key");
|
||||
});
|
||||
});
|
7
test/unit/api_key_test.rb
Normal file
7
test/unit/api_key_test.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
require 'test_helper'
|
||||
|
||||
class ApiKeyTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
Loading…
Reference in a new issue