diff --git a/app/api/app.rb b/app/api/app.rb new file mode 100644 index 0000000..fe594d9 --- /dev/null +++ b/app/api/app.rb @@ -0,0 +1,89 @@ +require_dependency 'api/helpers' + +module Api + class App < Grape::API + use Rack::JSONP + + logger = Logger.new(File.join(Rails.root, 'log', "#{ENV['RAILS_ENV'] || 'production'}_api.log")) + logger.formatter = GrapeLogging::Formatters::Default.new + #use GrapeLogging::Middleware::RequestLogger, { logger: logger } + + rescue_from :all do |e| + logger.error e + error! e.message, 500 + end + + @@NOOSFERO_CONF = nil + def self.NOOSFERO_CONF + if @@NOOSFERO_CONF + @@NOOSFERO_CONF + else + file = Rails.root.join('config', 'noosfero.yml') + @@NOOSFERO_CONF = File.exists?(file) ? YAML.load_file(file)[Rails.env] || {} : {} + end + end + + before { set_locale } + before { setup_multitenancy } + before { detect_stuff_by_domain } + before { filter_disabled_plugins_endpoints } + before { init_noosfero_plugins } + after { set_session_cookie } + + version 'v1' + prefix [ENV['RAILS_RELATIVE_URL_ROOT'], "api"].compact.join('/') + format :json + content_type :txt, "text/plain" + + helpers Helpers + + mount V1::Session + mount V1::Articles + mount V1::Comments + mount V1::Users + mount V1::Communities + mount V1::People + mount V1::Enterprises + mount V1::Categories + mount V1::Tasks + mount V1::Tags + mount V1::Environments + mount V1::Search + mount V1::Contacts + mount V1::Boxes + mount V1::Blocks + mount V1::Profiles + mount V1::Activities + + # hook point which allow plugins to add Grape::API extensions to Api::App + #finds for plugins which has api mount points classes defined (the class should extends Grape::API) + @plugins = Noosfero::Plugin.all.map { |p| p.constantize } + @plugins.each do |klass| + if klass.public_methods.include? :api_mount_points + klass.api_mount_points.each do |mount_class| + mount mount_class if mount_class && ( mount_class < Grape::API ) + end + end + end + + def self.endpoint_unavailable?(endpoint, environment) + api_class = endpoint.options[:app] || endpoint.options[:for] + if api_class.present? + klass = api_class.name.deconstantize.constantize + return klass < Noosfero::Plugin && !environment.plugin_enabled?(klass) + end + end + + class << self + def endpoints_with_plugins(environment = nil) + if environment.present? + cloned_endpoints = endpoints_without_plugins.dup + cloned_endpoints.delete_if { |endpoint| endpoint_unavailable?(endpoint, environment) } + else + endpoints_without_plugins + end + end + alias_method_chain :endpoints, :plugins + end + end +end diff --git a/app/api/entities.rb b/app/api/entities.rb new file mode 100644 index 0000000..8e095e3 --- /dev/null +++ b/app/api/entities.rb @@ -0,0 +1,267 @@ +module Api + module Entities + + Entity.format_with :timestamp do |date| + date.strftime('%Y/%m/%d %H:%M:%S') if date + end + + PERMISSIONS = { + :admin => 0, + :self => 10, + :private_content => 20, + :logged_user => 30, + :anonymous => 40 + } + + def self.can_display_profile_field? profile, options, permission_options={} + permissions={:field => "", :permission => :private_content} + permissions.merge!(permission_options) + field = permissions[:field] + permission = permissions[:permission] + return true if profile.public? && profile.public_fields.map{|f| f.to_sym}.include?(field.to_sym) + + current_person = options[:current_person] + + current_permission = if current_person.present? + if current_person.is_admin? + :admin + elsif current_person == profile + :self + elsif profile.display_private_info_to?(current_person) + :private_content + else + :logged_user + end + else + :anonymous + end + PERMISSIONS[current_permission] <= PERMISSIONS[permission] + end + + class Image < Entity + root 'images', 'image' + + expose :url do |image, options| + image.public_filename + end + + expose :icon_url do |image, options| + image.public_filename(:icon) + end + + expose :minor_url do |image, options| + image.public_filename(:minor) + end + + expose :portrait_url do |image, options| + image.public_filename(:portrait) + end + + expose :thumb_url do |image, options| + image.public_filename(:thumb) + end + end + + class CategoryBase < Entity + root 'categories', 'category' + expose :name, :id, :slug + end + + class Category < CategoryBase + root 'categories', 'category' + expose :full_name do |category, options| + category.full_name + end + expose :parent, :using => CategoryBase, if: { parent: true } + expose :children, :using => CategoryBase, if: { children: true } + expose :image, :using => Image + expose :display_color + end + + class Region < Category + root 'regions', 'region' + expose :parent_id + end + + class Block < Entity + root 'blocks', 'block' + expose :id, :type, :settings, :position, :enabled + expose :mirror, :mirror_block_id, :title + expose :api_content, if: lambda { |object, options| options[:display_api_content] || object.display_api_content_by_default? } + end + + class Box < Entity + root 'boxes', 'box' + expose :id, :position + expose :blocks, :using => Block + end + + class Profile < Entity + expose :identifier, :name, :id + expose :created_at, :format_with => :timestamp + expose :updated_at, :format_with => :timestamp + expose :additional_data do |profile, options| + hash ={} + profile.public_values.each do |value| + hash[value.custom_field.name]=value.value + end + + private_values = profile.custom_field_values - profile.public_values + private_values.each do |value| + if Entities.can_display_profile_field?(profile,options) + hash[value.custom_field.name]=value.value + end + end + hash + end + expose :image, :using => Image + expose :region, :using => Region + expose :type + expose :custom_header + expose :custom_footer + end + + class UserBasic < Entity + expose :id + expose :login + end + + class Person < Profile + root 'people', 'person' + expose :user, :using => UserBasic, documentation: {type: 'User', desc: 'The user data of a person' } + expose :vote_count + expose :comments_count do |person, options| + person.comments.count + end + expose :following_articles_count do |person, options| + person.following_articles.count + end + expose :articles_count do |person, options| + person.articles.count + end + end + + class Enterprise < Profile + root 'enterprises', 'enterprise' + end + + class Community < Profile + root 'communities', 'community' + expose :description + expose :admins, :if => lambda { |community, options| community.display_info_to? options[:current_person]} do |community, options| + community.admins.map{|admin| {"name"=>admin.name, "id"=>admin.id, "username" => admin.identifier}} + end + expose :categories, :using => Category + expose :members, :using => Person , :if => lambda{ |community, options| community.display_info_to? options[:current_person] } + end + + class CommentBase < Entity + expose :body, :title, :id + expose :created_at, :format_with => :timestamp + expose :author, :using => Profile + expose :reply_of, :using => CommentBase + end + + class Comment < CommentBase + root 'comments', 'comment' + expose :children, as: :replies, :using => Comment + end + + class ArticleBase < Entity + root 'articles', 'article' + expose :id + expose :body + expose :abstract, documentation: {type: 'String', desc: 'Teaser of the body'} + expose :created_at, :format_with => :timestamp + expose :updated_at, :format_with => :timestamp + expose :title, :documentation => {:type => "String", :desc => "Title of the article"} + expose :created_by, :as => :author, :using => Profile, :documentation => {type: 'Profile', desc: 'The profile author that create the article'} + expose :profile, :using => Profile, :documentation => {type: 'Profile', desc: 'The profile associated with the article'} + expose :categories, :using => Category + expose :image, :using => Image + expose :votes_for + expose :votes_against + expose :setting + expose :position + expose :hits + expose :start_date + expose :end_date, :documentation => {type: 'DateTime', desc: 'The date of finish of the article'} + expose :tag_list + expose :children_count + expose :slug, :documentation => {:type => "String", :desc => "Trimmed and parsed name of a article"} + expose :path + expose :followers_count + expose :votes_count + expose :comments_count + expose :archived, :documentation => {:type => "Boolean", :desc => "Defines if a article is readonly"} + expose :type + expose :comments, using: CommentBase, :if => lambda{|obj,opt| opt[:params] && ['1','true',true].include?(opt[:params][:show_comments])} + expose :published + expose :accept_comments?, as: :accept_comments + end + + class Article < ArticleBase + root 'articles', 'article' + expose :parent, :using => ArticleBase + expose :children, :using => ArticleBase do |article, options| + article.children.published.limit(V1::Articles::MAX_PER_PAGE) + end + end + + class User < Entity + root 'users', 'user' + + attrs = [:id,:login,:email,:activated?] + aliases = {:activated? => :activated} + + attrs.each do |attribute| + name = aliases.has_key?(attribute) ? aliases[attribute] : attribute + expose attribute, :as => name, :if => lambda{|user,options| Entities.can_display_profile_field?(user.person, options, {:field => attribute})} + end + + expose :person, :using => Person, :if => lambda{|user,options| user.person.display_info_to? options[:current_person]} + expose :permissions, :if => lambda{|user,options| Entities.can_display_profile_field?(user.person, options, {:field => :permissions, :permission => :self})} do |user, options| + output = {} + user.person.role_assignments.map do |role_assigment| + if role_assigment.resource.respond_to?(:identifier) && !role_assigment.role.nil? + output[role_assigment.resource.identifier] = role_assigment.role.permissions + end + end + output + end + end + + class UserLogin < User + root 'users', 'user' + expose :private_token, documentation: {type: 'String', desc: 'A valid authentication code for post/delete api actions'}, if: lambda {|object, options| object.activated? } + end + + class Task < Entity + root 'tasks', 'task' + expose :id + expose :type + end + + class Environment < Entity + expose :name + expose :id + expose :description + expose :settings, if: lambda { |instance, options| options[:is_admin] } + end + + class Tag < Entity + root 'tags', 'tag' + expose :name + end + + class Activity < Entity + root 'activities', 'activity' + expose :id, :params, :verb, :created_at, :updated_at, :comments_count, :visible + expose :user, :using => Profile + expose :target do |activity, opts| + type_map = {Profile => ::Profile, ArticleBase => ::Article}.find {|h| activity.target.kind_of?(h.last)} + type_map.first.represent(activity.target) unless type_map.nil? + end + end + end +end diff --git a/app/api/entity.rb b/app/api/entity.rb new file mode 100644 index 0000000..10c0192 --- /dev/null +++ b/app/api/entity.rb @@ -0,0 +1,27 @@ +module Api + class Entity < Grape::Entity + + def initialize(object, options = {}) + object = nil if object.is_a? Exception + super object, options + end + + def self.represent(objects, options = {}) + if options[:has_exception] + data = super objects, options.merge(is_inner_data: true) + if objects.is_a? Exception + data.merge ok: false, error: { + type: objects.class.name, + message: objects.message + } + else + data = data.serializable_hash if data.is_a? Entity + data.merge ok: true, error: { type: 'Success', message: '' } + end + else + super objects, options + end + end + + end +end diff --git a/app/api/helpers.rb b/app/api/helpers.rb new file mode 100644 index 0000000..04acdc3 --- /dev/null +++ b/app/api/helpers.rb @@ -0,0 +1,421 @@ +require 'base64' +require 'tempfile' + +module Api + module Helpers + PRIVATE_TOKEN_PARAM = :private_token + DEFAULT_ALLOWED_PARAMETERS = [:parent_id, :from, :until, :content_type, :author_id, :identifier, :archived] + + include SanitizeParams + include Noosfero::Plugin::HotSpot + include ForgotPasswordHelper + include SearchTermHelper + + def set_locale + I18n.locale = (params[:lang] || request.env['HTTP_ACCEPT_LANGUAGE'] || 'en') + end + + def init_noosfero_plugins + plugins + end + + def current_user + private_token = (params[PRIVATE_TOKEN_PARAM] || headers['Private-Token']).to_s + @current_user ||= User.find_by private_token: private_token + @current_user ||= plugins.dispatch("api_custom_login", request).first + @current_user + end + + def current_person + current_user.person unless current_user.nil? + end + + def is_admin?(environment) + return false unless current_user + return current_person.is_admin?(environment) + end + + def logout + @current_user = nil + end + + def environment + @environment + end + + def present_partial(model, options) + if(params[:fields].present?) + begin + fields = JSON.parse(params[:fields]) + if fields.present? + options.merge!(fields.symbolize_keys.slice(:only, :except)) + end + rescue + fields = params[:fields] + fields = fields.split(',') if fields.kind_of?(String) + options[:only] = Array.wrap(fields) + end + end + present model, options + end + + include FindByContents + + #################################################################### + #### SEARCH + #################################################################### + def multiple_search?(searches=nil) + ['index', 'category_index'].include?(params[:action]) || (searches && searches.size > 1) + end + #################################################################### + + def logger + logger = Logger.new(File.join(Rails.root, 'log', "#{ENV['RAILS_ENV'] || 'production'}_api.log")) + logger.formatter = GrapeLogging::Formatters::Default.new + logger + end + + def limit + limit = params[:limit].to_i + limit = default_limit if limit <= 0 + limit + end + + def period(from_date, until_date) + return nil if from_date.nil? && until_date.nil? + + begin_period = from_date.nil? ? Time.at(0).to_datetime : from_date + end_period = until_date.nil? ? DateTime.now : until_date + + begin_period..end_period + end + + def parse_content_type(content_type) + return nil if content_type.blank? + content_type.split(',').map do |content_type| + content_type.camelcase + end + end + + def find_article(articles, id) + article = articles.find(id) + article.display_to?(current_person) ? article : forbidden! + end + + def post_article(asset, params) + return forbidden! unless current_person.can_post_content?(asset) + + klass_type = params[:content_type] || params[:article].delete(:type) || TinyMceArticle.name + return forbidden! unless klass_type.constantize <= Article + + article = klass_type.constantize.new(params[:article]) + article.last_changed_by = current_person + article.created_by= current_person + article.profile = asset + + if !article.save + render_api_errors!(article.errors.full_messages) + end + present_partial article, :with => Entities::Article + end + + def present_article(asset) + article = find_article(asset.articles, params[:id]) + present_partial article, :with => Entities::Article, :params => params + end + + def present_articles_for_asset(asset, method = 'articles') + articles = find_articles(asset, method) + present_articles(articles) + end + + def present_articles(articles) + present_partial paginate(articles), :with => Entities::Article, :params => params + end + + def find_articles(asset, method = 'articles') + articles = select_filtered_collection_of(asset, method, params) + if current_person.present? + articles = articles.display_filter(current_person, nil) + else + articles = articles.published + end + articles + end + + def find_task(asset, id) + task = asset.tasks.find(id) + current_person.has_permission?(task.permission, asset) ? task : forbidden! + end + + def post_task(asset, params) + klass_type= params[:content_type].nil? ? 'Task' : params[:content_type] + return forbidden! unless klass_type.constantize <= Task + + task = klass_type.constantize.new(params[:task]) + task.requestor_id = current_person.id + task.target_id = asset.id + task.target_type = 'Profile' + + if !task.save + render_api_errors!(task.errors.full_messages) + end + present_partial task, :with => Entities::Task + end + + def present_task(asset) + task = find_task(asset, params[:id]) + present_partial task, :with => Entities::Task + end + + def present_tasks(asset) + tasks = select_filtered_collection_of(asset, 'tasks', params) + tasks = tasks.select {|t| current_person.has_permission?(t.permission, asset)} + return forbidden! if tasks.empty? && !current_person.has_permission?(:perform_task, asset) + present_partial tasks, :with => Entities::Task + end + + def make_conditions_with_parameter(params = {}) + parsed_params = parser_params(params) + conditions = {} + from_date = DateTime.parse(parsed_params.delete(:from)) if parsed_params[:from] + until_date = DateTime.parse(parsed_params.delete(:until)) if parsed_params[:until] + + conditions[:type] = parse_content_type(parsed_params.delete(:content_type)) unless parsed_params[:content_type].nil? + + conditions[:created_at] = period(from_date, until_date) if from_date || until_date + conditions.merge!(parsed_params) + + conditions + end + + # changing make_order_with_parameters to avoid sql injection + def make_order_with_parameters(object, method, params) + order = "created_at DESC" + unless params[:order].blank? + if params[:order].include? '\'' or params[:order].include? '"' + order = "created_at DESC" + elsif ['RANDOM()', 'RANDOM'].include? params[:order].upcase + order = 'RANDOM()' + else + field_name, direction = params[:order].split(' ') + assoc = object.class.reflect_on_association(method.to_sym) + if !field_name.blank? and assoc + if assoc.klass.attribute_names.include? field_name + if direction.present? and ['ASC','DESC'].include? direction.upcase + order = "#{field_name} #{direction.upcase}" + end + end + end + end + end + return order + end + + def make_timestamp_with_parameters_and_method(params, method) + timestamp = nil + if params[:timestamp] + datetime = DateTime.parse(params[:timestamp]) + table_name = method.to_s.singularize.camelize.constantize.table_name + timestamp = "#{table_name}.updated_at >= '#{datetime}'" + end + + timestamp + end + + def by_reference(scope, params) + reference_id = params[:reference_id].to_i == 0 ? nil : params[:reference_id].to_i + if reference_id.nil? + scope + else + created_at = scope.find(reference_id).created_at + scope.send("#{params.key?(:oldest) ? 'older_than' : 'younger_than'}", created_at) + end + end + + def by_categories(scope, params) + category_ids = params[:category_ids] + if category_ids.nil? + scope + else + scope.joins(:categories).where(:categories => {:id => category_ids}) + end + end + + def select_filtered_collection_of(object, method, params) + conditions = make_conditions_with_parameter(params) + order = make_order_with_parameters(object,method,params) + timestamp = make_timestamp_with_parameters_and_method(params, method) + + objects = object.send(method) + objects = by_reference(objects, params) + objects = by_categories(objects, params) + + objects = objects.where(conditions).where(timestamp).reorder(order) + + params[:page] ||= 1 + params[:per_page] ||= limit + paginate(objects) + end + + def authenticate! + unauthorized! unless current_user + end + + def profiles_for_person(profiles, person) + if person + profiles.listed_for_person(person) + else + profiles.visible + end + end + + # Checks the occurrences of uniqueness of attributes, each attribute must be present in the params hash + # or a Bad Request error is invoked. + # + # Parameters: + # keys (unique) - A hash consisting of keys that must be unique + def unique_attributes!(obj, keys) + keys.each do |key| + cant_be_saved_request!(key) if obj.find_by(key.to_s => params[key]) + end + end + + def attributes_for_keys(keys) + attrs = {} + keys.each do |key| + attrs[key] = params[key] if params[key].present? or (params.has_key?(key) and params[key] == false) + end + attrs + end + + ########################################## + # error helpers # + ########################################## + + def not_found! + render_api_error!('404 Not found', 404) + end + + def forbidden! + render_api_error!('403 Forbidden', 403) + end + + def cant_be_saved_request!(attribute) + message = _("(Invalid request) %s can't be saved") % attribute + render_api_error!(message, 400) + end + + def bad_request!(attribute) + message = _("(Invalid request) %s not given") % attribute + render_api_error!(message, 400) + end + + def something_wrong! + message = _("Something wrong happened") + render_api_error!(message, 400) + end + + def unauthorized! + render_api_error!(_('Unauthorized'), 401) + end + + def not_allowed! + render_api_error!(_('Method Not Allowed'), 405) + end + + # javascript_console_message is supposed to be executed as console.log() + def render_api_error!(user_message, status, log_message = nil, javascript_console_message = nil) + message_hash = {'message' => user_message, :code => status} + message_hash[:javascript_console_message] = javascript_console_message if javascript_console_message.present? + log_msg = "#{status}, User message: #{user_message}" + log_msg = "#{log_message}, #{log_msg}" if log_message.present? + log_msg = "#{log_msg}, Javascript Console Message: #{javascript_console_message}" if javascript_console_message.present? + logger.error log_msg unless Rails.env.test? + error!(message_hash, status) + end + + def render_api_errors!(messages) + messages = messages.to_a if messages.class == ActiveModel::Errors + render_api_error!(messages.join(','), 400) + end + + protected + + def set_session_cookie + cookies['_noosfero_api_session'] = { value: @current_user.private_token, httponly: true } if @current_user.present? + end + + def setup_multitenancy + Noosfero::MultiTenancy.setup!(request.host) + end + + def detect_stuff_by_domain + @domain = Domain.by_name(request.host) + if @domain.nil? + @environment = Environment.default + if @environment.nil? && Rails.env.development? + # This should only happen in development ... + @environment = Environment.create!(:name => "Noosfero", :is_default => true) + end + else + @environment = @domain.environment + end + end + + def filter_disabled_plugins_endpoints + not_found! if Api::App.endpoint_unavailable?(self, @environment) + end + + def asset_with_image params + if params.has_key? :image_builder + asset_api_params = params + asset_api_params[:image_builder] = base64_to_uploadedfile(asset_api_params[:image_builder]) + return asset_api_params + end + params + end + + def base64_to_uploadedfile(base64_image) + tempfile = base64_to_tempfile base64_image + converted_image = base64_image + converted_image[:tempfile] = tempfile + return {uploaded_data: ActionDispatch::Http::UploadedFile.new(converted_image)} + end + + def base64_to_tempfile base64_image + base64_img_str = base64_image[:tempfile] + decoded_base64_str = Base64.decode64(base64_img_str) + tempfile = Tempfile.new(base64_image[:filename]) + tempfile.write(decoded_base64_str.encode("ascii-8bit").force_encoding("utf-8")) + tempfile.rewind + tempfile + end + private + + def parser_params(params) + parsed_params = {} + params.map do |k,v| + parsed_params[k.to_sym] = v if DEFAULT_ALLOWED_PARAMETERS.include?(k.to_sym) + end + parsed_params + end + + def default_limit + 20 + end + + def parse_content_type(content_type) + return nil if content_type.blank? + content_type.split(',').map do |content_type| + content_type.camelcase + end + end + + def period(from_date, until_date) + begin_period = from_date.nil? ? Time.at(0).to_datetime : from_date + end_period = until_date.nil? ? DateTime.now : until_date + begin_period..end_period + end + end +end diff --git a/app/api/v1/activities.rb b/app/api/v1/activities.rb new file mode 100644 index 0000000..ce697d4 --- /dev/null +++ b/app/api/v1/activities.rb @@ -0,0 +1,20 @@ +module Api + module V1 + class Activities < Grape::API + before { authenticate! } + + resource :profiles do + + get ':id/activities' do + profile = Profile.find_by id: params[:id] + + not_found! if profile.blank? || profile.secret || !profile.visible + forbidden! if !profile.secret && profile.visible && !profile.display_private_info_to?(current_person) + + activities = profile.activities.map(&:activity) + present activities, :with => Entities::Activity, :current_person => current_person + end + end + end + end +end diff --git a/app/api/v1/articles.rb b/app/api/v1/articles.rb new file mode 100644 index 0000000..2d5908a --- /dev/null +++ b/app/api/v1/articles.rb @@ -0,0 +1,303 @@ +module Api + module V1 + class Articles < Grape::API + + ARTICLE_TYPES = Article.descendants.map{|a| a.to_s} + + MAX_PER_PAGE = 50 + + resource :articles do + + paginate max_per_page: MAX_PER_PAGE + # Collect articles + # + # Parameters: + # from - date where the search will begin. If nothing is passed the default date will be the date of the first article created + # oldest - Collect the oldest articles. If nothing is passed the newest articles are collected + # limit - amount of articles returned. The default value is 20 + # + # Example Request: + # GET host/api/v1/articles?from=2013-04-04-14:41:43&until=2015-04-04-14:41:43&limit=10&private_token=e96fff37c2238fdab074d1dcea8e6317 + + desc 'Return all articles of all kinds' do + detail 'Get all articles filtered by fields in query params' + params Entities::Article.documentation + success Entities::Article + failure [[403, 'Forbidden']] + named 'ArticlesList' + headers [ + 'Per-Page' => { + description: 'Total number of records', + required: false + } + ] + end + get do + present_articles_for_asset(environment) + end + + desc "Return one article by id" do + detail 'Get only one article by id. If not found the "forbidden" http error is showed' + params Entities::Article.documentation + success Entities::Article + failure [[403, 'Forbidden']] + named 'ArticleById' + end + get ':id', requirements: {id: /[0-9]+/} do + present_article(environment) + end + + post ':id' do + article = environment.articles.find(params[:id]) + return forbidden! unless article.allow_edit?(current_person) + article.update_attributes!(asset_with_image(params[:article])) + present_partial article, :with => Entities::Article + end + + desc 'Report a abuse and/or violent content in a article by id' do + detail 'Submit a abuse (in general, a content violation) report about a specific article' + params Entities::Article.documentation + failure [[400, 'Bad Request']] + named 'ArticleReportAbuse' + end + post ':id/report_abuse' do + article = find_article(environment.articles, params[:id]) + profile = article.profile + begin + abuse_report = AbuseReport.new(:reason => params[:report_abuse]) + if !params[:content_type].blank? + article = params[:content_type].constantize.find(params[:content_id]) + abuse_report.content = article_reported_version(article) + end + + current_person.register_report(abuse_report, profile) + + if !params[:content_type].blank? + abuse_report = AbuseReport.find_by reporter_id: current_person.id, abuse_complaint_id: profile.opened_abuse_complaint.id + Delayed::Job.enqueue DownloadReportedImagesJob.new(abuse_report, article) + end + + { + :success => true, + :message => _('Your abuse report was registered. The administrators are reviewing your report.'), + } + rescue Exception => exception + #logger.error(exception.to_s) + render_api_error!(_('Your report couldn\'t be saved due to some problem. Please contact the administrator.'), 400) + end + + end + + desc "Returns the articles I voted" do + detail 'Get the Articles I make a vote' + failure [[403, 'Forbidden']] + named 'ArticleFollowers' + end + #FIXME refactor this method + get 'voted_by_me' do + present_articles(current_person.votes.where(:voteable_type => 'Article').collect(&:voteable)) + end + + desc 'Perform a vote on a article by id' do + detail 'Vote on a specific article with values: 1 (if you like) or -1 (if not)' + params Entities::UserLogin.documentation + failure [[401,'Unauthorized']] + named 'ArticleVote' + end + post ':id/vote' do + authenticate! + value = (params[:value] || 1).to_i + # FIXME verify allowed values + render_api_error!('Vote value not allowed', 400) unless [-1, 1].include?(value) + article = find_article(environment.articles, params[:id]) + begin + vote = Vote.new(:voteable => article, :voter => current_person, :vote => value) + {:vote => vote.save!} + rescue ActiveRecord::RecordInvalid => e + render_api_error!(e.message, 400) + end + end + + desc "Returns the total followers for the article" do + detail 'Get the followers of a specific article by id' + failure [[403, 'Forbidden']] + named 'ArticleFollowers' + end + get ':id/followers' do + article = find_article(environment.articles, params[:id]) + total = article.person_followers.count + {:total_followers => total} + end + + desc "Return the articles followed by me" + get 'followed_by_me' do + present_articles_for_asset(current_person, 'following_articles') + end + + desc "Add a follower for the article" do + detail 'Add the current user identified by private token, like a follower of a article' + params Entities::UserLogin.documentation + failure [[401, 'Unauthorized']] + named 'ArticleFollow' + end + post ':id/follow' do + authenticate! + article = find_article(environment.articles, params[:id]) + if article.article_followers.exists?(:person_id => current_person.id) + {:success => false, :already_follow => true} + else + article_follower = ArticleFollower.new + article_follower.article = article + article_follower.person = current_person + article_follower.save! + {:success => true} + end + end + + desc 'Return the children of a article identified by id' do + detail 'Get all children articles of a specific article' + params Entities::Article.documentation + failure [[403, 'Forbidden']] + named 'ArticleChildren' + end + + paginate per_page: MAX_PER_PAGE, max_per_page: MAX_PER_PAGE + get ':id/children' do + article = find_article(environment.articles, params[:id]) + + #TODO make tests for this situation + votes_order = params.delete(:order) if params[:order]=='votes_score' + articles = select_filtered_collection_of(article, 'children', params) + articles = articles.display_filter(current_person, article.profile) + + #TODO make tests for this situation + if votes_order + articles = articles.joins('left join votes on articles.id=votes.voteable_id').group('articles.id').reorder('sum(coalesce(votes.vote, 0)) DESC') + end + Article.hit(articles) + present_articles(articles) + end + + desc 'Return one child of a article identified by id' do + detail 'Get a child of a specific article' + params Entities::Article.documentation + success Entities::Article + failure [[403, 'Forbidden']] + named 'ArticleChild' + end + get ':id/children/:child_id' do + article = find_article(environment.articles, params[:id]) + child = find_article(article.children, params[:child_id]) + child.hit + present_partial child, :with => Entities::Article + end + + desc 'Suggest a article to another profile' do + detail 'Suggest a article to another profile (person, community...)' + params Entities::Article.documentation + success Entities::Task + failure [[401,'Unauthorized']] + named 'ArticleSuggest' + end + post ':id/children/suggest' do + authenticate! + parent_article = environment.articles.find(params[:id]) + + suggest_article = SuggestArticle.new + suggest_article.article = params[:article] + suggest_article.article[:parent_id] = parent_article.id + suggest_article.target = parent_article.profile + suggest_article.requestor = current_person + + unless suggest_article.save + render_api_errors!(suggest_article.article_object.errors.full_messages) + end + present_partial suggest_article, :with => Entities::Task + end + + # Example Request: + # POST api/v1/articles/:id/children?private_token=234298743290432&article[name]=title&article[body]=body + desc 'Add a child article to a parent identified by id' do + detail 'Create a new article and associate to a parent' + params Entities::Article.documentation + success Entities::Article + failure [[401,'Unauthorized']] + named 'ArticleAddChild' + end + post ':id/children' do + parent_article = environment.articles.find(params[:id]) + params[:article][:parent_id] = parent_article.id + post_article(parent_article.profile, params) + end + end + + resource :profiles do + get ':id/home_page' do + profiles = environment.profiles + profiles = profiles.visible_for_person(current_person) + profile = profiles.find_by id: params[:id] + present_partial profile.home_page, :with => Entities::Article + end + end + + kinds = %w[profile community person enterprise] + kinds.each do |kind| + resource kind.pluralize.to_sym do + segment "/:#{kind}_id" do + resource :articles do + + desc "Return all articles associate with a profile of type #{kind}" do + detail 'Get a list of articles of a profile' + params Entities::Article.documentation + success Entities::Article + failure [[403, 'Forbidden']] + named 'ArticlesOfProfile' + end + get do + profile = environment.send(kind.pluralize).find(params["#{kind}_id"]) + + if params[:path].present? + article = profile.articles.find_by path: params[:path] + if !article || !article.display_to?(current_person) + article = forbidden! + end + + present_partial article, :with => Entities::Article + else + + present_articles_for_asset(profile) + end + end + + desc "Return a article associate with a profile of type #{kind}" do + detail 'Get only one article of a profile' + params Entities::Article.documentation + success Entities::Article + failure [[403, 'Forbidden']] + named 'ArticleOfProfile' + end + get ':id' do + profile = environment.send(kind.pluralize).find(params["#{kind}_id"]) + present_article(profile) + end + + # Example Request: + # POST api/v1/{people,communities,enterprises}/:asset_id/articles?private_token=234298743290432&article[name]=title&article[body]=body + desc "Add a new article associated with a profile of type #{kind}" do + detail 'Create a new article and associate with a profile' + params Entities::Article.documentation + success Entities::Article + failure [[403, 'Forbidden']] + named 'ArticleCreateToProfile' + end + post do + profile = environment.send(kind.pluralize).find(params["#{kind}_id"]) + post_article(profile, params) + end + end + end + end + end + end + end +end diff --git a/app/api/v1/blocks.rb b/app/api/v1/blocks.rb new file mode 100644 index 0000000..6a84d22 --- /dev/null +++ b/app/api/v1/blocks.rb @@ -0,0 +1,15 @@ +module Api + module V1 + + class Blocks < Grape::API + resource :blocks do + get ':id' do + block = Block.find(params["id"]) + return forbidden! unless block.visible_to_user?(current_person) + present block, :with => Entities::Block, display_api_content: true + end + end + end + + end +end diff --git a/app/api/v1/boxes.rb b/app/api/v1/boxes.rb new file mode 100644 index 0000000..f669d83 --- /dev/null +++ b/app/api/v1/boxes.rb @@ -0,0 +1,44 @@ +module Api + module V1 + + class Boxes < Grape::API + + kinds = %w[profile community person enterprise] + kinds.each do |kind| + + resource kind.pluralize.to_sym do + + segment "/:#{kind}_id" do + resource :boxes do + get do + profile = environment.send(kind.pluralize).find(params["#{kind}_id"]) + present profile.boxes, :with => Entities::Box + end + end + end + end + + end + + resource :environments do + [ '/default', '/context', ':environment_id' ].each do |route| + segment route do + resource :boxes do + get do + if (route.match(/default/)) + env = Environment.default + elsif (route.match(/context/)) + env = environment + else + env = Environment.find(params[:environment_id]) + end + present env.boxes, :with => Entities::Box + end + end + end + end + end + end + + end +end diff --git a/app/api/v1/categories.rb b/app/api/v1/categories.rb new file mode 100644 index 0000000..91d217d --- /dev/null +++ b/app/api/v1/categories.rb @@ -0,0 +1,25 @@ +module Api + module V1 + class Categories < Grape::API + + resource :categories do + + get do + type = params[:category_type] + include_parent = params[:include_parent] == 'true' + include_children = params[:include_children] == 'true' + + categories = type.nil? ? environment.categories : environment.categories.where(:type => type) + present categories, :with => Entities::Category, parent: include_parent, children: include_children + end + + desc "Return the category by id" + get ':id' do + present environment.categories.find(params[:id]), :with => Entities::Category, parent: true, children: true + end + + end + + end + end +end diff --git a/app/api/v1/comments.rb b/app/api/v1/comments.rb new file mode 100644 index 0000000..4952e77 --- /dev/null +++ b/app/api/v1/comments.rb @@ -0,0 +1,49 @@ +module Api + module V1 + class Comments < Grape::API + MAX_PER_PAGE = 20 + + + resource :articles do + paginate max_per_page: MAX_PER_PAGE + # Collect comments from articles + # + # Parameters: + # reference_id - comment id used as reference to collect comment + # oldest - Collect the oldest comments from reference_id comment. If nothing is passed the newest comments are collected + # limit - amount of comments returned. The default value is 20 + # + # Example Request: + # GET /articles/12/comments?oldest&limit=10&reference_id=23 + get ":id/comments" do + article = find_article(environment.articles, params[:id]) + comments = select_filtered_collection_of(article, :comments, params) + comments = comments.without_spam + comments = comments.without_reply if(params[:without_reply].present?) + comments = plugins.filter(:unavailable_comments, comments) + present paginate(comments), :with => Entities::Comment, :current_person => current_person + end + + get ":id/comments/:comment_id" do + article = find_article(environment.articles, params[:id]) + present article.comments.find(params[:comment_id]), :with => Entities::Comment, :current_person => current_person + end + + # Example Request: + # POST api/v1/articles/12/comments?private_token=2298743290432&body=new comment&title=New + post ":id/comments" do + authenticate! + article = find_article(environment.articles, params[:id]) + options = params.select { |key,v| !['id','private_token'].include?(key) }.merge(:author => current_person, :source => article) + begin + comment = Comment.create!(options) + rescue ActiveRecord::RecordInvalid => e + render_api_error!(e.message, 400) + end + present comment, :with => Entities::Comment, :current_person => current_person + end + end + + end + end +end diff --git a/app/api/v1/communities.rb b/app/api/v1/communities.rb new file mode 100644 index 0000000..0dcb9a0 --- /dev/null +++ b/app/api/v1/communities.rb @@ -0,0 +1,82 @@ +module Api + module V1 + class Communities < Grape::API + + resource :communities do + + # Collect comments from articles + # + # Parameters: + # from - date where the search will begin. If nothing is passed the default date will be the date of the first article created + # oldest - Collect the oldest comments from reference_id comment. If nothing is passed the newest comments are collected + # limit - amount of comments returned. The default value is 20 + # + # Example Request: + # GET /communities?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10 + # GET /communities?reference_id=10&limit=10&oldest + get do + communities = select_filtered_collection_of(environment, 'communities', params) + communities = profiles_for_person(communities, current_person) + communities = communities.by_location(params) # Must be the last. May return Exception obj + present communities, :with => Entities::Community, :current_person => current_person + end + + + # Example Request: + # POST api/v1/communties?private_token=234298743290432&community[name]=some_name + # for each custom field for community, add &community[field_name]=field_value to the request + post do + authenticate! + params[:community] ||= {} + + params[:community][:custom_values]={} + params[:community].keys.each do |key| + params[:community][:custom_values][key]=params[:community].delete(key) if Community.custom_fields(environment).any?{|cf| cf.name==key} + end + + begin + community = Community.create_after_moderation(current_person, params[:community].merge({:environment => environment})) + rescue + community = Community.new(params[:community]) + end + + if !community.save + render_api_errors!(community.errors.full_messages) + end + + present community, :with => Entities::Community, :current_person => current_person + end + + get ':id' do + community = profiles_for_person(environment.communities, current_person).find_by_id(params[:id]) + present community, :with => Entities::Community, :current_person => current_person + end + + end + + resource :people do + + segment '/:person_id' do + + resource :communities do + + get do + person = environment.people.find(params[:person_id]) + + not_found! if person.blank? + forbidden! if !person.display_info_to?(current_person) + + communities = select_filtered_collection_of(person, 'communities', params) + communities = communities.visible + present communities, :with => Entities::Community, :current_person => current_person + end + + end + + end + + end + + end + end +end diff --git a/app/api/v1/contacts.rb b/app/api/v1/contacts.rb new file mode 100644 index 0000000..839484b --- /dev/null +++ b/app/api/v1/contacts.rb @@ -0,0 +1,26 @@ +module Api + module V1 + class Contacts < Grape::API + + resource :communities do + + resource ':id/contact' do + #contact => {:name => 'some name', :email => 'test@mail.com', :subject => 'some title', :message => 'some message'} + desc "Send a contact message" + post do + profile = environment.communities.find(params[:id]) + forbidden! unless profile.present? + contact = Contact.new params[:contact].merge(dest: profile) + if contact.deliver + {:success => true} + else + {:success => false} + end + end + + end + end + + end + end +end diff --git a/app/api/v1/enterprises.rb b/app/api/v1/enterprises.rb new file mode 100644 index 0000000..7d7cca0 --- /dev/null +++ b/app/api/v1/enterprises.rb @@ -0,0 +1,55 @@ +module Api + module V1 + class Enterprises < Grape::API + + resource :enterprises do + + # Collect enterprises from environment + # + # Parameters: + # from - date where the search will begin. If nothing is passed the default date will be the date of the first article created + # oldest - Collect the oldest comments from reference_id comment. If nothing is passed the newest comments are collected + # limit - amount of comments returned. The default value is 20 + # georef params - read `Profile.by_location` for more information. + # + # Example Request: + # GET /enterprises?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10 + # GET /enterprises?reference_id=10&limit=10&oldest + get do + enterprises = select_filtered_collection_of(environment, 'enterprises', params) + enterprises = enterprises.visible + enterprises = enterprises.by_location(params) # Must be the last. May return Exception obj. + present enterprises, :with => Entities::Enterprise, :current_person => current_person + end + + desc "Return one enterprise by id" + get ':id' do + enterprise = environment.enterprises.visible.find_by(id: params[:id]) + present enterprise, :with => Entities::Enterprise, :current_person => current_person + end + + end + + resource :people do + + segment '/:person_id' do + + resource :enterprises do + + get do + person = environment.people.find(params[:person_id]) + enterprises = select_filtered_collection_of(person, 'enterprises', params) + enterprises = enterprises.visible.by_location(params) + present enterprises, :with => Entities::Enterprise, :current_person => current_person + end + + end + + end + + end + + + end + end +end diff --git a/app/api/v1/environments.rb b/app/api/v1/environments.rb new file mode 100644 index 0000000..3551781 --- /dev/null +++ b/app/api/v1/environments.rb @@ -0,0 +1,28 @@ +module Api + module V1 + class Environments < Grape::API + + resource :environment do + + desc "Return the person information" + get '/signup_person_fields' do + present environment.signup_person_fields + end + + get ':id' do + local_environment = nil + if (params[:id] == "default") + local_environment = Environment.default + elsif (params[:id] == "context") + local_environment = environment + else + local_environment = Environment.find(params[:id]) + end + present_partial local_environment, :with => Entities::Environment, :is_admin => is_admin?(local_environment) + end + + end + + end + end +end diff --git a/app/api/v1/people.rb b/app/api/v1/people.rb new file mode 100644 index 0000000..d12626d --- /dev/null +++ b/app/api/v1/people.rb @@ -0,0 +1,127 @@ +module Api + module V1 + class People < Grape::API + + MAX_PER_PAGE = 50 + + desc 'API Root' + + resource :people do + paginate max_per_page: MAX_PER_PAGE + + # -- A note about privacy -- + # We wold find people by location, but we must test if the related + # fields are public. We can't do it now, with SQL, while the location + # data and the fields_privacy are a serialized settings. + # We must build a new table for profile data, where we can set meta-data + # like: + # | id | profile_id | key | value | privacy_level | source | + # | 1 | 99 | city | Salvador | friends | user | + # | 2 | 99 | lng | -38.521 | me only | automatic | + + # Collect people from environment + # + # Parameters: + # from - date where the search will begin. If nothing is passed the default date will be the date of the first article created + # oldest - Collect the oldest comments from reference_id comment. If nothing is passed the newest comments are collected + # limit - amount of comments returned. The default value is 20 + # + # Example Request: + # GET /people?from=2013-04-04-14:41:43&until=2014-04-04-14:41:43&limit=10 + # GET /people?reference_id=10&limit=10&oldest + + desc "Find environment's people" + get do + people = select_filtered_collection_of(environment, 'people', params) + people = people.visible + present_partial people, :with => Entities::Person, :current_person => current_person + end + + desc "Return the logged user information" + get "/me" do + authenticate! + present_partial current_person, :with => Entities::Person, :current_person => current_person + end + + desc "Return the person information" + get ':id' do + person = environment.people.visible.find_by(id: params[:id]) + return not_found! if person.blank? + present person, :with => Entities::Person, :current_person => current_person + end + + desc "Update person information" + post ':id' do + authenticate! + return forbidden! if current_person.id.to_s != params[:id] + current_person.update_attributes!(asset_with_image(params[:person])) + present current_person, :with => Entities::Person, :current_person => current_person + end + + # POST api/v1/people?person[login]=some_login&person[password]=some_password&person[name]=Jack + # for each custom field for person, add &person[field_name]=field_value to the request + desc "Create person" + post do + authenticate! + user_data = {} + user_data[:login] = params[:person].delete(:login) || params[:person][:identifier] + user_data[:email] = params[:person].delete(:email) + user_data[:password] = params[:person].delete(:password) + user_data[:password_confirmation] = params[:person].delete(:password_confirmation) + + params[:person][:custom_values]={} + params[:person].keys.each do |key| + params[:person][:custom_values][key]=params[:person].delete(key) if Person.custom_fields(environment).any?{|cf| cf.name==key} + end + + user = User.build(user_data, asset_with_image(params[:person]), environment) + + begin + user.signup! + rescue ActiveRecord::RecordInvalid + render_api_errors!(user.errors.full_messages) + end + + present user.person, :with => Entities::Person, :current_person => user.person + end + + desc "Return the person friends" + get ':id/friends' do + person = environment.people.visible.find_by(id: params[:id]) + return not_found! if person.blank? + friends = person.friends.visible + present friends, :with => Entities::Person, :current_person => current_person + end + + desc "Return the person permissions on other profiles" + get ":id/permissions" do + authenticate! + person = environment.people.find(params[:id]) + return not_found! if person.blank? + return forbidden! unless current_person == person || environment.admins.include?(current_person) + + output = {} + person.role_assignments.map do |role_assigment| + if role_assigment.resource.respond_to?(:identifier) + output[role_assigment.resource.identifier] = role_assigment.role.permissions + end + end + present output + end + end + + resource :profiles do + segment '/:profile_id' do + resource :members do + paginate max_per_page: MAX_PER_PAGE + get do + profile = environment.profiles.find_by id: params[:profile_id] + members = select_filtered_collection_of(profile, 'members', params) + present members, :with => Entities::Person, :current_person => current_person + end + end + end + end + end + end +end diff --git a/app/api/v1/profiles.rb b/app/api/v1/profiles.rb new file mode 100644 index 0000000..9678229 --- /dev/null +++ b/app/api/v1/profiles.rb @@ -0,0 +1,42 @@ +module Api + module V1 + class Profiles < Grape::API + + resource :profiles do + + get do + profiles = select_filtered_collection_of(environment, 'profiles', params) + profiles = profiles.visible + profiles = profiles.by_location(params) # Must be the last. May return Exception obj. + present profiles, :with => Entities::Profile, :current_person => current_person + end + + get ':id' do + profiles = environment.profiles + profiles = profiles.visible + profile = profiles.find_by id: params[:id] + + if profile + present profile, :with => Entities::Profile, :current_person => current_person + else + not_found! + end + end + + delete ':id' do + authenticate! + profiles = environment.profiles + profile = profiles.find_by id: params[:id] + + not_found! if profile.blank? + + if current_person.has_permission?(:destroy_profile, profile) + profile.destroy + else + forbidden! + end + end + end + end + end +end diff --git a/app/api/v1/search.rb b/app/api/v1/search.rb new file mode 100644 index 0000000..8594d10 --- /dev/null +++ b/app/api/v1/search.rb @@ -0,0 +1,37 @@ +module Api + module V1 + class Search < Grape::API + + resource :search do + resource :article do + paginate max_per_page: 200 + get do + # Security checks + sanitize_params_hash(params) + # Api::Helpers + asset = :articles + context = environment + + profile = environment.profiles.find(params[:profile_id]) if params[:profile_id] + scope = profile.nil? ? environment.articles.is_public : profile.articles.is_public + scope = scope.where(:type => params[:type]) if params[:type] && !(params[:type] == 'Article') + scope = scope.where(make_conditions_with_parameter(params)) + scope = scope.joins(:categories).where(:categories => {:id => params[:category_ids]}) if params[:category_ids].present? + scope = scope.where('articles.children_count > 0') if params[:has_children].present? + query = params[:query] || "" + order = "more_recent" + + options = {:filter => order, :template_id => params[:template_id]} + + search_result = find_by_contents(asset, context, scope, query, {:page => 1}, options) + + articles = search_result[:results] + + present_articles(articles) + end + end + end + + end + end +end diff --git a/app/api/v1/session.rb b/app/api/v1/session.rb new file mode 100644 index 0000000..7d02ef2 --- /dev/null +++ b/app/api/v1/session.rb @@ -0,0 +1,157 @@ +require "uri" + +module Api + module V1 + class Session < Grape::API + + # Login to get token + # + # Parameters: + # login (*required) - user login or email + # password (required) - user password + # + # Example Request: + # POST http://localhost:3000/api/v1/login?login=adminuser&password=admin + post "/login" do + begin + user ||= User.authenticate(params[:login], params[:password], environment) + rescue User::UserNotActivated => e + render_api_error!(e.message, 401) + end + + return unauthorized! unless user + @current_user = user + present user, :with => Entities::UserLogin, :current_person => current_person + end + + post "/login_from_cookie" do + return unauthorized! if cookies[:auth_token].blank? + user = User.where(remember_token: cookies[:auth_token]).first + return unauthorized! unless user && user.activated? + @current_user = user + present user, :with => Entities::UserLogin, :current_person => current_person + end + + # Create user. + # + # Parameters: + # email (required) - Email + # password (required) - Password + # login - login + # Example Request: + # POST /register?email=some@mail.com&password=pas&password_confirmation=pas&login=some + params do + requires :email, type: String, desc: _("Email") + requires :login, type: String, desc: _("Login") + requires :password, type: String, desc: _("Password") + end + + post "/register" do + attrs = attributes_for_keys [:email, :login, :password, :password_confirmation] + environment.signup_person_fields + name = params[:name].present? ? params[:name] : attrs[:email] + attrs[:password_confirmation] = attrs[:password] if !attrs.has_key?(:password_confirmation) + user = User.new(attrs.merge(:name => name)) + + begin + user.signup! + user.generate_private_token! if user.activated? + present user, :with => Entities::UserLogin, :current_person => user.person + rescue ActiveRecord::RecordInvalid + message = user.errors.as_json.merge((user.person.present? ? user.person.errors : {}).as_json).to_json + render_api_error!(message, 400) + end + end + + params do + requires :activation_code, type: String, desc: _("Activation token") + end + + # Activate a user. + # + # Parameter: + # activation_code (required) - Activation token + # Example Request: + # PATCH /activate?activation_code=28259abd12cc6a64ef9399cf3286cb998b96aeaf + patch "/activate" do + user = User.find_by activation_code: params[:activation_code] + if user + unless user.environment.enabled?('admin_must_approve_new_users') + if user.activate + user.generate_private_token! + present user, :with => Entities::UserLogin, :current_person => current_person + end + else + if user.create_moderate_task + user.activation_code = nil + user.save! + + # Waiting for admin moderate user registration + status 202 + body({ + :message => 'Waiting for admin moderate user registration' + }) + end + end + else + # Token not found in database + render_api_error!(_('Token is invalid'), 412) + end + end + + # Request a new password. + # + # Parameters: + # value (required) - Email or login + # Example Request: + # POST /forgot_password?value=some@mail.com + post "/forgot_password" do + requestors = fetch_requestors(params[:value]) + not_found! if requestors.blank? + remote_ip = (request.respond_to?(:remote_ip) && request.remote_ip) || (env && env['REMOTE_ADDR']) + requestors.each do |requestor| + ChangePassword.create!(:requestor => requestor) + end + end + + # Resend activation code. + # + # Parameters: + # value (required) - Email or login + # Example Request: + # POST /resend_activation_code?value=some@mail.com + post "/resend_activation_code" do + requestors = fetch_requestors(params[:value]) + not_found! if requestors.blank? + remote_ip = (request.respond_to?(:remote_ip) && request.remote_ip) || (env && env['REMOTE_ADDR']) + requestors.each do |requestor| + requestor.user.resend_activation_code + end + present requestors.map(&:user), :with => Entities::UserLogin + end + + params do + requires :code, type: String, desc: _("Forgot password code") + end + # Change password + # + # Parameters: + # code (required) - Change password code + # password (required) + # password_confirmation (required) + # Example Request: + # PATCH /new_password?code=xxxx&password=secret&password_confirmation=secret + patch "/new_password" do + change_password = ChangePassword.find_by code: params[:code] + not_found! if change_password.nil? + + if change_password.update_attributes(:password => params[:password], :password_confirmation => params[:password_confirmation]) + change_password.finish + present change_password.requestor.user, :with => Entities::UserLogin, :current_person => current_person + else + something_wrong! + end + end + + end + end +end diff --git a/app/api/v1/tags.rb b/app/api/v1/tags.rb new file mode 100644 index 0000000..cdfa380 --- /dev/null +++ b/app/api/v1/tags.rb @@ -0,0 +1,26 @@ +module Api + module V1 + class Tags < Grape::API + before { authenticate! } + + resource :articles do + + resource ':id/tags' do + + get do + article = find_article(environment.articles, params[:id]) + present article.tag_list + end + + desc "Add a tag to an article" + post do + article = find_article(environment.articles, params[:id]) + article.tag_list=params[:tags] + article.save + present article.tag_list + end + end + end + end + end +end diff --git a/app/api/v1/tasks.rb b/app/api/v1/tasks.rb new file mode 100644 index 0000000..ea4ca40 --- /dev/null +++ b/app/api/v1/tasks.rb @@ -0,0 +1,57 @@ +module Api + module V1 + class Tasks < Grape::API +# before { authenticate! } + +# ARTICLE_TYPES = Article.descendants.map{|a| a.to_s} + + resource :tasks do + + # Collect tasks + # + # Parameters: + # from - date where the search will begin. If nothing is passed the default date will be the date of the first article created + # oldest - Collect the oldest articles. If nothing is passed the newest articles are collected + # limit - amount of articles returned. The default value is 20 + # + # Example Request: + # GET host/api/v1/tasks?from=2013-04-04-14:41:43&until=2015-04-04-14:41:43&limit=10&private_token=e96fff37c2238fdab074d1dcea8e6317 + get do + tasks = select_filtered_collection_of(environment, 'tasks', params) + tasks = tasks.select {|t| current_person.has_permission?(t.permission, environment)} + present_partial tasks, :with => Entities::Task + end + + desc "Return the task id" + get ':id' do + task = find_task(environment, params[:id]) + present_partial task, :with => Entities::Task + end + end + + kinds = %w[community person enterprise] + kinds.each do |kind| + resource kind.pluralize.to_sym do + segment "/:#{kind}_id" do + resource :tasks do + get do + profile = environment.send(kind.pluralize).find(params["#{kind}_id"]) + present_tasks(profile) + end + + get ':id' do + profile = environment.send(kind.pluralize).find(params["#{kind}_id"]) + present_task(profile) + end + + post do + profile = environment.send(kind.pluralize).find(params["#{kind}_id"]) + post_task(profile, params) + end + end + end + end + end + end + end +end diff --git a/app/api/v1/users.rb b/app/api/v1/users.rb new file mode 100644 index 0000000..a5a6303 --- /dev/null +++ b/app/api/v1/users.rb @@ -0,0 +1,43 @@ +module Api + module V1 + class Users < Grape::API + + resource :users do + + get do + users = select_filtered_collection_of(environment, 'users', params) + users = users.select{|u| u.person.display_info_to? current_person} + present users, :with => Entities::User, :current_person => current_person + end + + get "/me" do + authenticate! + present current_user, :with => Entities::User, :current_person => current_person + end + + get ":id" do + user = environment.users.find_by id: params[:id] + if user + present user, :with => Entities::User, :current_person => current_person + else + not_found! + end + end + + get ":id/permissions" do + authenticate! + user = environment.users.find(params[:id]) + output = {} + user.person.role_assignments.map do |role_assigment| + if role_assigment.resource.respond_to?(:identifier) && role_assigment.resource.identifier == params[:profile] + output[:permissions] = role_assigment.role.permissions + end + end + present output + end + + end + + end + end +end diff --git a/app/controllers/public/api_controller.rb b/app/controllers/public/api_controller.rb index cb1404f..d76a890 100644 --- a/app/controllers/public/api_controller.rb +++ b/app/controllers/public/api_controller.rb @@ -13,7 +13,7 @@ class ApiController < PublicController private def endpoints - Noosfero::API::API.endpoints(environment) + Api::App.endpoints(environment) end end diff --git a/app/models/recent_documents_block.rb b/app/models/recent_documents_block.rb index fd05945..e156199 100644 --- a/app/models/recent_documents_block.rb +++ b/app/models/recent_documents_block.rb @@ -31,7 +31,7 @@ class RecentDocumentsBlock < Block end def api_content - Noosfero::API::Entities::ArticleBase.represent(docs).as_json + Api::Entities::ArticleBase.represent(docs).as_json end def display_api_content_by_default? diff --git a/app/views/api/playground.html.erb b/app/views/api/playground.html.erb index 9edbea7..6624f8e 100644 --- a/app/views/api/playground.html.erb +++ b/app/views/api/playground.html.erb @@ -1,7 +1,7 @@