From 01755d4ed8b5c56c06d1ab1ff181b114a83eb496 Mon Sep 17 00:00:00 2001 From: "OHASHI, Norikazu" Date: Mon, 18 Sep 2023 13:56:20 +0900 Subject: [PATCH] =?utf8?q?=E7=8F=BE=E7=8A=B6=E3=81=AE=E7=8A=B6=E6=85=8B?= =?utf8?q?=E3=82=92commit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .ruby-version | 1 + .yardopts | 7 +- client/src/.tern-port | 2 +- restapi.apib | 45 +- sinatra/Gemfile | 1 + sinatra/app/controllers/restful_api.rb | 607 ++++++++++++++++++++++--- sinatra/app/controllers/spa_gui.rb | 3 +- sinatra/app/controllers/web_gui.rb | 22 +- sinatra/app/models/books_db.rb | 90 +++- sinatra/app/models/tokens_db.rb | 6 +- sinatra/app/models/users_db.rb | 2 - sinatra/app/views/layout.haml | 8 +- 13 files changed, 711 insertions(+), 85 deletions(-) create mode 100644 .ruby-version diff --git a/.gitignore b/.gitignore index 13b4438..a692666 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ /sinatra/Gemfile.lock /sinatra/vendor/ /sinatra/public/scripts/ +/sinatre/public/css/ /client/node_modules/ *.log *.org +restapi.html diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..6a81b4c --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.7.8 diff --git a/.yardopts b/.yardopts index 9fedcb7..764c0a9 100644 --- a/.yardopts +++ b/.yardopts @@ -1,7 +1,12 @@ --plugin yard-sinatra ---type-name-tag post_param:"POST Parameters" --type-name-tag path_param:"Path Paramaters" +--type-name-tag query_param:"Query Parameters" +--type-name-tag post_param:"POST Parameters" +--type-name-tag request_param:"Parameters with request body" +--type-name-tag response_param:"Parameters with response body" --markup-provider=redcarpet --markup=markdown +sinatra/app/controllers/*.rb +sinatra/app/models/*.rb - readme.md diff --git a/client/src/.tern-port b/client/src/.tern-port index b6c93a1..01c29ac 100644 --- a/client/src/.tern-port +++ b/client/src/.tern-port @@ -1 +1 @@ -57153 \ No newline at end of file +49939 \ No newline at end of file diff --git a/restapi.apib b/restapi.apib index 5ba1649..580ba02 100644 --- a/restapi.apib +++ b/restapi.apib @@ -117,6 +117,14 @@ HOST: http://www.book-server2.neko-mori.org/webapi/v1 対象のユーザ情報が存在しない。 + Attributes (Error) ++ Response 403 (application/json) +操作権限がない。 + + Attributes (Error) + ++ Response 401 (application/json) +認証失敗 + + Attributes (Error) + #### ユーザ削除 [DELETE] ユーザ情報の削除を行う (要管理者権限) @@ -172,16 +180,16 @@ HOST: http://www.book-server2.neko-mori.org/webapi/v1 + Attributes (Error) + Response 406 (application/json) -すでに同一ISDNが蔵書一覧に登録済み +すでに同一ISBNが蔵書一覧に登録済み + Attributes (Error) -### 蔵書個別 [/book_collections/{isdn}{?token}] +### 蔵書個別 [/book_collections/{isbn}{?token}] + Parameter - + isdn: `978412345678` (string, required) - 対象書籍のISDN + + isbn: `978412345678` (string, required) - 対象書籍のISBN + token: `5dd5eopo3kpXO19YWVgP8A` (string, required) - APIアクセス用のトークン #### 蔵書詳細 [GET] -指定したISDNの蔵書情報を取得 +指定したISBNの蔵書情報を取得 + Response 200 (application/json) 正常終了 @@ -196,10 +204,10 @@ HOST: http://www.book-server2.neko-mori.org/webapi/v1 + Attributes (Error) #### 蔵書更新 [PUT] -指定したISDNの蔵書情報の更新 +指定したISBNの蔵書情報の更新 + Request (application/Json) - + Attributes (BookCollectionRegistData) + + Attributes (BookCollectionUpdateData) + Response 200 (application/json) 正常終了 @@ -215,7 +223,7 @@ HOST: http://www.book-server2.neko-mori.org/webapi/v1 #### 蔵書削除 [DELETE] -指定したISDNの蔵書情報の蔵書一覧からの削除 +指定したISBNの蔵書情報の蔵書一覧からの削除 + Response 200 (application/json) 正常終了 @@ -247,13 +255,13 @@ HOST: http://www.book-server2.neko-mori.org/webapi/v1 セッション切れ + Attributes (Error) -### 書籍個別 [/books/{isdn}{?token}] +### 書籍個別 [/books/{isbn}{?token}] + Parameters - + isdn: `978412345678` (string, required) - 対象書籍のISDN + + isbn: `978412345678` (string, required) - 対象書籍のISBN + token: `5dd5eopo3kpXO19YWVgP8A` (string, required) - APIアクセス用のトークン #### 書籍詳細取得 [GET] -ISDNで指定された書籍の詳細情報を取得する。システムに未登録の場合 Internet 上から取得する。 +ISBNで指定された書籍の詳細情報を取得する。システムに未登録の場合 Internet 上から取得する。 + Response 200 (application/json) 正常終了 @@ -268,7 +276,7 @@ ISDNで指定された書籍の詳細情報を取得する。システムに未 + Attributes (Error) #### 書籍情報の削除 [DELETE] -ISDNで指定された書籍の全情報を削除する。 (管理者権限でのみ実施可能) +ISBNで指定された書籍の全情報を削除する。 (管理者権限でのみ実施可能) + Response 200 (application/json) 正常終了 @@ -302,7 +310,10 @@ ISDNで指定された書籍の全情報を削除する。 (管理者権限で + user_role: `8` (number, required) - ユーザの権限 [0: 管理者権限, 8: 一般権限, 10: 無効] ## Book -+ isdn: `978412345678` (string, required) - 対象書籍のISDN ++ isbn: `978412345678` (string, required) - 対象書籍のISBN ++ Include UpdateBook + +## UpdateBook + title: `羅生門` (string, required) - 書籍のタイトル + volume: `0` (number, optional) - 書籍の巻数 + subtitle: `人間の業` (string, optional) - 書籍のサブタイトル @@ -322,8 +333,14 @@ ISDNで指定された書籍の全情報を削除する。 (管理者権限で + summary: `羅城門は芥川の書いた作品の...` (string, optional) - 書籍の説明 + book_rank: `5` (number,optional) - 書籍の評価値 +## BookCollectionUpdateData ++ Include UpdateBook ++ cover_data: `data:image/png;base64,q/9j/4AAQSkZJRgABAQAAAQABAAD...` (string, optional) - カバー画のBAS64encode ++ summary: `羅城門は芥川の書いた作品の...` (string, optional) - 書籍の説明 ++ book_rank: `5` (number,optional) - 書籍の評価値 + ## BookListItem -+ isdn: `978412345678` (string, required) - 対象書籍の ++ isbn: `978412345678` (string, required) - 対象書籍のISBN + title: `羅生門` (string, required) - 書籍のタイトル + volume: `0` (number, optional) - 書籍の巻数 + author: `芥川龍之介` (string, optional) - 著者名 @@ -339,3 +356,5 @@ ISDNで指定された書籍の全情報を削除する。 (管理者権限で ## Error + code: `E0000000` (string,required) - エラーコード ++ message: `Error message.` (string,required) - エラーメッセージ + diff --git a/sinatra/Gemfile b/sinatra/Gemfile index 37f4e1b..88fa6d2 100644 --- a/sinatra/Gemfile +++ b/sinatra/Gemfile @@ -20,4 +20,5 @@ gem 'sass' gem 'scss' gem 'coffee-script' gem 'unicorn' +gem 'data_uri' # gem 'mini_racer' diff --git a/sinatra/app/controllers/restful_api.rb b/sinatra/app/controllers/restful_api.rb index b76cae0..7a11998 100644 --- a/sinatra/app/controllers/restful_api.rb +++ b/sinatra/app/controllers/restful_api.rb @@ -11,7 +11,7 @@ require 'sinatra/json' require 'logger' require 'json' require 'securerandom' - +require 'data_uri' # ユーザアクセス用モデル require_relative '../models/users_db' @@ -48,32 +48,90 @@ class RESTfulAPI < Sinatra::Base @tokens = TokenManager.instance end + helpers do + + # 更新用の書籍情報を作成する。 + # @param [Hash] params Postで取得した全パラメータ + # @param [Boolean] image_f 書影データの有無 + # @return [Hash] 更新用書籍情報 + def makeBookInfo(params) + book_info = {} + params.each do |key, value| + case key + when 'summary', 'book_rank', 'cover_data' then + # 対象キーは書籍情報ではないので飛す + next + end + book_info[key.to_sym] = value + end + return book_info + end + + # 書影情報の作成 + # @param [String] cover_data 書影データのData URI scheme(bas64) + # @return [Hash] 作成した書影データ + def setBookImage(cover_data) + cover_image = {} + # 書影情報の取得 + data_uri = URI::Data.new(cover_data) + cover_image[:mime_type] = data_uri.content_type + cover_image[:data] = data_uri.data + return cover_image + end + end + + + # ユーザログイン + # @path_param [String] version API Version + # @example Request body + # { + # "user_name": "user01", + # "password": "password" + # } + post '/:version/login' do begin + # パラメータ取得 body = JSON.parse(request.body.read) name = body['user_name'] passwd = body['password'] + + # ログイン情報確認 id = @user_account.checkPasswd(name, passwd) + # トークン取得 token = @tokens.getToken(id) logger.info("Get session of book server. id: #{id}, user_name: #{name}, token #{token}") + + # レスポンスボディー作成 response = {:token => token, :id => id} json response + rescue UserAccount::NotFoundInstanceError, UserAccount::AuthenticationError - logger.error("Invaild user or password. user_name: #{name}") - raise WebError.new(:status => 401, :code => 'E01001') + + # 認証失敗 + raise WebError.new(:status => 401, :code => 'E01001', + :message => "Invaild user or password. user_name: #{name}") + rescue JSON::ParserError => e - logger.error("Failed to parse JSON. #{e.message}") - raise WebError.new(:status => 400, :code => 'E01002') + + # JSONパースエラー + raise WebError.new(:status => 400, :code => 'E01002', + :message => "Failed to parse JSON. #{e.message}") end end # ユーザーログアウト + # @path_param [String] version API Version + # @query_param [String] token APIアクセス用トークン + delete '/:version/logout' do begin + # トークからユーザーIDの取得 token = params[:token] @tokens.releseToken(token) + logger.info("Logout session of book server. token: #{token}") response = {} json response @@ -81,17 +139,33 @@ class RESTfulAPI < Sinatra::Base end # ユーザ一覧取得 + # @path_param [String] version API Version + # @query_param [String] token APIアクセス用トークン + # @example Response Body + # [ + # { + # "id": 15, + # "user_name": "user01", + # "full_name": "Taro Suzuki", + # "email": "user01@book-server2.neko-mori.org", + # "user_role": 8 + # } + # ] get '/:version/users' do begin + # トークンからユーザーIDの取得 token = params[:token] id = @token.getId(token) # 管理者権限でなければ利用できない - raise WebError.new(:status => 403, :code => "E02001") unless - @user_account.checkRole(id, [User::ROLE_ADMIN]) + raise WebError.new(:status => 403, :code => "E02001", + :message => "User is without authority. userid=#{id}" + ) unless @user_account.checkRole(id, [User::ROLE_ADMIN]) + # ユーザ一覧の取得 users = @user_account.getUserList + # レスポンスボディーの生成 response = users.map {|u| { :id => u.user_id, :user_name => u.user_name, :full_name => u.full_name, :email => u.emaiil, @@ -101,53 +175,96 @@ class RESTfulAPI < Sinatra::Base rescue TokenManager::UnknownTokenError, TokenManager::ExpiredTokenError - logger.error("Session expired of #{token}") - raise WebError.new(:status => 408, :code => "E02002") + + # トークン認証失敗 + raise WebError.new(:status => 408, :code => "E02002", + :message => "Session expired of #{token}") end end #ユーザ登録 - put '/:version/users' do + # @path_param [String] version API Version + # @query_param [String] tonken APIアクセス用トークン + # @example Request Body + # { + # "user_name": "user01", + # "full_name": "Taro Suzuki", + # "email": "user01@book-server2.neko-mori.org", + # "user_role": 8, + # "password": "password00" + # } + post '/:version/users' do begin + # トークンからユーザーIDの取得 token = params[:token] id = @token.getId(token) # 管理者権限でなければ利用できない - raise WebError.new(:status => 403, :code => "E03001") unless - @user_account.checkRole(id, [User::ROLE_ADMIN, User::ROLE_APPUSE]) - + raise WebError.new(:status => 403, :code => "E03001", + :message => "User is without authority. userid=#{id}" + ) unless @user_account.checkRole(id, [User::ROLE_ADMIN, User::ROLE_APPUSE]) + + # リクエストボディのJSONをパースする。 body = JSON.parse(request.body.read) - create_id = createAccount(body['user_name'], body['full_name'], + # ユーザ作成 + create_id = createAccount(body['user_name'], body['full_name'], body['emaiil'], body['password'], body['user_role']); - + + # レスポンスボディーを生成 response={:id => create_id} json response rescue TokenManager::UnknownTokenError, TokenManager::ExpiredTokenError - logger.error("Session expired of #{token}") - raise WebError.new(:status => 408, :code => "E03002") + + # トークン認証失敗 + raise WebError.new(:status => 408, :code => "E03002", + :message => "Session expired of #{token}") + rescue UserAccount::AlreadyInstanceError - logger.error("Already exist user_name #{body['user_name']}") - raise WebError.new(:status => 408, :code => "E03003") + + # すでに同名のユーザーが登録済み + logger.error() + raise WebError.new(:status => 408, :code => "E03003", + :message => "Already exist user_name #{body['user_name']}") + rescue JSON::ParserError => e - logger.error("Failed to parse JSON. #{e.message}") - raise WebError.new(:status => 400, :code => 'E03004') + + # JSONパースエラー + logger.error() + raise WebError.new(:status => 400, :code => 'E03004', + :message => "Failed to parse JSON. #{e.message}") end end # ユーザ詳細 + # @path_param [String] version API Version + # @path_param [String] id 取得対象のユーザーID + # @query_param [String] token APIアクセス用トークン + # @example Response Body + # { + # "id": 15, + # "user_name": "user01", + # "full_name": "Taro Suzuki", + # "email": "user01@book-server2.neko-mori.org", + # "user_role": 8 + # } get '/:version/users/:id' do begin + #パラメータ取得 token = params[:token] read_id = params[:id] + # トークンからユーザーIDの取得 id = @token.getId(token) - raise WebError.new(:status => 403, :code => "E04001") if - !@user_account.checkRole(id, [User::ROLE_ADMIN]) && id != read_id - + # 管理者権限かログインしているユーザでなければ利用できない + raise WebError.new(:status => 403, :code => "E04001", + :message => "User is without authority. userid=#{id}" + ) if !@user_account.checkRole(id, [User::ROLE_ADMIN]) && id != read_id + + # ユーザ情報を取得してレソポンスボディー用の情報に変換 user = @user_account.getUser(id) response = { :id => user.user_id, :user_name => user.user_name, :full_name => user.full_name, :email => user.emaiil, @@ -156,29 +273,51 @@ class RESTfulAPI < Sinatra::Base rescue TokenManager::UnknownTokenError, TokenManager::ExpiredTokenError - logger.error("Session expired of #{token}") - raise WebError.new(:status => 408, :code => "E04002") + + # トークン認証失敗 + raise WebError.new(:status => 408, :code => "E04002", + :message => "Session expired of #{token}") + rescue UserAccount::NotFoundInstanceError - logger.error("Not found user. user id: #{update_id}") - raise WebError.new(:status => 404, :code => "E04003") + + # 対象ユーザーの情報がない + raise WebError.new(:status => 404, :code => "E04003", + :message => "Not found user. user id: #{update_id}") end end - # ユーザ詳細 + # ユーザ更新 + # @path_param [String] version API Version + # @path_param [String] id 取得対象のユーザーID + # @query_param [String] tonken APIアクセス用トークン + # @example Request Body + # { + # "user_name": "user01", + # "full_name": "Taro Suzuki", + # "email": "user01@book-server2.neko-mori.org", + # "user_role": 8, + # "password": "password00", + # "orignal_password": "oldPassword" + # } put '/:version/users/:id' do begin + #パラメータ取得 token = params[:token] update_id = params[:id] + # トークンからユーザーIDの取得 id = @token.getId(token) - raise WebError.new(:status => 403, :code => "E05001") if - !@user_account.checkRole(id, [User::ROLE_ADMIN]) && id != update_id - + # 管理者権限かログインしているユーザでなければ利用できない + raise WebError.new(:status => 403, :code => "E05001", + :message => "User is without authority. userid=#{id}" + ) if !@user_account.checkRole(id, [User::ROLE_ADMIN]) && id != update_id body = JSON.parse(request.body.read) - raise WebError.new(:status => 401, :code => "E05002") if - id == update_id && !checkPasswd!(id, body['orignal_password']) + # パスワード更新で元パスワードが異なる + raise WebError.new(:status => 401, :code => "E05002", + :message => "Authentication failed. userid=#{id}") if + !@user_account.checkPasswdOfId(id, body['orignal_password']) params = { :passwd => body['password'], :user_name => body['user_name'], :full_name => body['full_name'], :email => body['email'] } @@ -189,22 +328,38 @@ class RESTfulAPI < Sinatra::Base rescue TokenManager::UnknownTokenError, TokenManager::ExpiredTokenError - logger.error("Session expired of #{token}") - raise WebError.new(:status => 408, :code => "E05003") + + # トークン認証失敗 + raise WebError.new(:status => 408, :code => "E05003", + :message => "Session expired of #{token}") + rescue UserAccount::NotFoundInstanceError - logger.error("Not found user. user id: #{update_id}") - raise WebError.new(:status => 404, :code => "E05004") + + # 対象ユーザーの情報がない + raise WebError.new(:status => 404, :code => "E05004", :message => "Not found user. user id: #{update_id}") + + rescue JSON::ParserError => e + + # JSONパースエラー + raise WebError.new(:status => 400, :code => 'E05005', + :message => "Failed to parse JSON. #{e.message}") end end + # ユーザ削除 + # @path_param [String] version API Version + # @path_param [String] id 取得対象のユーザーID + # @query_param [String] tonken APIアクセス用トークン delete '/:version/users/:id' do begin token = params[:token] delete_id = params[:id] id = @token.getId(token) - raise WebError.new(:status => 403, :code => "E06001") unless - @user_account.checkRole(id, [User::ROLE_ADMIN]) + # 管理者権限でなければ利用できない + raise WebError.new(:status => 403, :code => "E06001", + :message => "User is without authority. userid=#{id}" + ) unless @user_account.checkRole(id, [User::ROLE_ADMIN]) @user_account.deleteUser(delete_id) @@ -213,27 +368,46 @@ class RESTfulAPI < Sinatra::Base rescue TokenManager::UnknownTokenError, TokenManager::ExpiredTokenError - logger.error("Session expired of #{token}") - raise WebError.new(:status => 408, :code => "E06002") + + # トークン認証失敗 + raise WebError.new(:status => 408, :code => "E06002", + :message => "Session expired of #{token}") + rescue UserAccount::NotFoundInstanceError - logger.error("Not found user. user id: #{update_id}") - raise WebError.new(:status => 404, :code => "E06003") + + # 対象ユーザーの情報がない + raise WebError.new(:status => 404, :code => "E06003", + :message => "Not found user. user id: #{update_id}") end end + # ユーザ所持の蔵書一覧取得 + # @path_param [String] version API Version + # @query_param [String] tonken APIアクセス用トークン + # @query_param [String] search 検索条件 + # @example Response Body + # [ + # { + # "isbn": "978412345678", + # "title": "羅生門", + # "volume": 0, + # "author": "芥川龍之介", + # "orignal_author": "芥川龍之介", + # "illustrator": "奥山誠司", + # "publisher": "角川文庫", + # "pubdate": "2012/01/20" + # } + # ] get '/:version/book_collections' do begin token = params[:token] - search = params[:search] - + search = JSON.perse(params[:search]) id = @token.getId(token) - raise WebError.new(:status => 403, :code => "E06001") unless - @user_account.checkRole(id, [User::ROLE_ADMIN]) - book_list, full_size = @book_manager.narrowBooOfId(id,0,0, search) + book_list, full_size = @book_manager.narrowBookOfId(id,0,0, search) if full_size > 0 response = book_list.map { |book| { - :isdn => book.isdn, :title => book.title, :volume => book.volume, + :isbn => book.isbn, :title => book.title, :volume => book.volume, :author => book.author, :original_author => book.original_author, :illustrator => book.illustrator, :publisher => book.publisher, :pubdate => book.pubdate, @@ -243,14 +417,345 @@ class RESTfulAPI < Sinatra::Base response = [] end json response + + rescue TokenManager::UnknownTokenError, + TokenManager::ExpiredTokenError + + # トークン認証失敗 + raise WebError.new(:status => 408, :code => "E07002", + :message => "Session expired of #{token}") + + rescue JSON::ParserError => e + + # JSONパースエラー + raise WebError.new(:status => 400, :code => 'E07003', + :message => "Failed to parse JSON. #{e.message}") + end + end + + # ユーザ所持の書籍の登録 + # @path_param [String] version API Version + # @query_param [String] tonken APIアクセス用トークン + # @example Request Body + # { + # "isbn": "978412345678", + # "title": "羅生門", + # "volume": 0, + # "subtitle": "人間の業", + # "series": "芥川全集", + # "author": "芥川龍之介", + # "orignal_author": "芥川龍之介", + # "illustrator": "奥山誠司", + # "translator": "ジェイ・ルービン", + # "supervisor": "角川春樹", + # "publisher": "角川文庫", + # "pubdate": "2012/01/20", + # "cover_uri": "https://cover.openbd.jp/9784107723369.jpg", + # "cover_data": "data:image/png;base64,q/9j/4AAQSkZJRgABAQAAAQABAAD...", + # "summary": "羅城門は芥川の書いた作品の...", + # "book_rank": 5 + # } + post '/:version/book_collections' do + begin + token = params[:token] + id = @token.getId(token) + book = JSON.parse(request.body.read) + + @book_manager.createCollectBook(id, book) + response = {} + json response + + rescue TokenManager::UnknownTokenError, + TokenManager::ExpiredTokenError + + raise WebError.new(:status => 408, :code => "E08001", + :message => "Session expired of #{token}") + + rescue BookManager::AlreadyInstanceError + + raise WebError.new(:status => 406, :code => "E08002", + :message => "Already exsit target book. isbn=#{isbn}") + end + end + + # ユーザ所持の蔵書詳細取得 + # @path_param [String] version API Version + # @path_param [String] isbn 対象書籍のISBN + # @query_param [String] tonken APIアクセス用トークン + # @example Response Body + # { + # "isbn": "978412345678", + # "title": "羅生門", + # "volume": 0, + # "subtitle": "人間の業", + # "series": "芥川全集", + # "author": "芥川龍之介", + # "orignal_author": "芥川龍之介", + # "illustrator": "奥山誠司", + # "translator": "ジェイ・ルービン", + # "supervisor": "角川春樹", + # "publisher": "角川文庫", + # "pubdate": "2012/01/20", + # "cover_uri": "https://cover.openbd.jp/9784107723369.jpg", + # "summary": "羅城門は芥川の書いた作品の...", + # "book_rank": 5 + # } + get '/:version/book_collections/:isbn' do + begin + token = params[:token] + isbn = params[:isbn] + id = @token.getId(token) + response = @book_manager.getBookCollect(isbn, id) + json response + + rescue TokenManager::UnknownTokenError, + TokenManager::ExpiredTokenError + + # トークン認証失敗 + raise WebError.new(:status => 408, :code => "E09001", + :message => "Session expired of #{token}") + + rescue BookManager::NotFoundInstanceError + + # 対象の書籍が存在しない + raise WebError.new(:status => 404, :code => "E09002", + :message => "Not found target book. isbn=#{isbn}") + + end + end + + # ユーザ所持の蔵書更新 + # @path_param [String] version API Version + # @path_param [String] isbn 対象書籍のISBN + # @query_param [String] tonken APIアクセス用トークン + # @example Request Body + # { + # "title": "羅生門", + # "volume": 0, + # "subtitle": "人間の業", + # "series": "芥川全集", + # "author": "芥川龍之介", + # "orignal_author": "芥川龍之介", + # "illustrator": "奥山誠司", + # "translator": "ジェイ・ルービン", + # "supervisor": "角川春樹", + # "publisher": "角川文庫", + # "pubdate": "2012/01/20", + # "cover_uri": "https://cover.openbd.jp/9784107723369.jpg", + # "cover_data": "data:image/png;base64,q/9j/4AAQSkZJRgABAQAAAQABAAD...", + # "summary": "羅城門は芥川の書いた作品の...", + # "book_rank": 5 + # } + put '/:version/book_collections/:isbn' do + begin + token = params[:token] + isbn = params[:isbn] + id = @token.getId(token) + book = JSON.parse(request.body.read) + @book_manager.updateCollectBook(id, book) + + response = {} + json response + + rescue TokenManager::UnknownTokenError, + TokenManager::ExpiredTokenError + + #トークン認証失敗 + raise WebError.new(:status => 408, :code => "E10001", + :message => "Session expired of #{token}") + + rescue BookManager::NotFoundInstanceError + + # 対象の書籍が存在しない + raise WebError.new(:status => 404, :code => "E10002", + :message => "Not found target book. isbn=#{isbn}") + + rescue JSON::ParserError => e + + # JSONパースエラー + raise WebError.new(:status => 400, :code => 'E10003', + :message => "Failed to parse JSON. #{e.message}") + end + end + + # ユーザ所持の蔵書詳細取得 + # @path_param [String] version API Version + # @path_param [String] isbn 対象書籍のISBN + # @query_param [String] tonken APIアクセス用トークン + delete '/:version/book_collections/:isbn' do + begin + token = params[:token] + isbn = params[:isbn] + id = @token.getId(token) + @book_manager.deleteBookCollect(isbn, id) + response = {} + json response + + rescue TokenManager::UnknownTokenError, + TokenManager::ExpiredTokenError + + # トークン認証失敗 + raise WebError.new(:status => 408, :code => "E11001", + :message => "Session expired of #{token}") + + rescue BookManager::NotFoundInstanceError + + # 対象の書籍が存在しない + raise WebError.new(:status => 404, :code => "E11002", + :message => "Not found target book. isbn=#{isbn}") + end end + # 登録書籍の一覧取得 + # @path_param [String] version API Version + # @query_param [String] tonken APIアクセス用トークン + # @query_param [Boolean] nobody 所有者がいない書籍の一覧取得時に true + # @example Response Body + # [ + # { + # "isbn": "978412345678", + # "title": "羅生門", + # "volume": 0, + # "author": "芥川龍之介", + # "orignal_author": "芥川龍之介", + # "illustrator": "奥山誠司", + # "publisher": "角川文庫", + # "pubdate": "2012/01/20" + # } + # ] + get '/:version/books' do + begin + token = params[:token] + nobody = params[:nobody] + id = @token.getId(token) + # 管理者権限でなければ利用できない + raise WebError.new(:status => 403, :code => "E12001", + :message => "User is without authority. userid=#{id}" + ) unless @user_account.checkRole(id, [User::ROLE_ADMIN]) + book_list = @book_manager.getBookList(nobody == true) + response = book_list.map { |book| + { + :isbn => book.isbn, :title => book.title, :volume => book.volume, + :author => book.author, :original_author => book.original_author, + :illustrator => book.illustrator, :publisher => book.publisher, + :pubdate => book.pubdate, + } + } + json response + + rescue TokenManager::UnknownTokenError, + TokenManager::ExpiredTokenError + + # トークン認証失敗 + raise WebError.new(:status => 408, :code => "E12002", + :message => "Session expired of #{token}") + + end + end + + # 登録書籍の詳細取得 + # @path_param [String] version API Version + # @path_param [String] isbn 対象書籍のISBN + # @query_param [String] tonken APIアクセス用トークン + # @example Response Body + # { + # "isbn": "978412345678", + # "title": "羅生門", + # "volume": 0, + # "subtitle": "人間の業", + # "series": "芥川全集", + # "author": "芥川龍之介", + # "orignal_author": "芥川龍之介", + # "illustrator": "奥山誠司", + # "translator": "ジェイ・ルービン", + # "supervisor": "角川春樹", + # "publisher": "角川文庫", + # "pubdate": "2012/01/20", + # "cover_uri": "https://cover.openbd.jp/9784107723369.jpg" + # } + get '/:version/books/:isbn' do + begin + token = params[:token] + isbn = params[:isbn] + id = @token.getId(token) + # 管理者権限でなければ利用できない + raise WebError.new(:status => 403, :code => "E13001", + :message => "User is without authority. userid=#{id}" + ) unless @user_account.checkRole(id, [User::ROLE_ADMIN]) + response = @book_manager.getBook(isbn) + json response + + rescue TokenManager::UnknownTokenError, + TokenManager::ExpiredTokenError + + # トークン認証失敗 + raise WebError.new(:status => 408, :code => "E13002", + :message => "Session expired of #{token}") + + rescue BookManager::NotFoundInstanceError + + # 対象の書籍が存在しない + raise WebError.new(:status => 404, :code => "E13002", + :message => "Not found target book. isbn=#{isbn}") + + end + end + + # 登録書籍の詳細取得 + # @path_param [String] version API Version + # @path_param [String] isbn 対象書籍のISBN + # @query_param [String] tonken APIアクセス用トークン + # @example Response Body + # { + # "isbn": "978412345678", + # "title": "羅生門", + # "volume": 0, + # "subtitle": "人間の業", + # "series": "芥川全集", + # "author": "芥川龍之介", + # "orignal_author": "芥川龍之介", + # "illustrator": "奥山誠司", + # "translator": "ジェイ・ルービン", + # "supervisor": "角川春樹", + # "publisher": "角川文庫", + # "pubdate": "2012/01/20", + # "cover_uri": "https://cover.openbd.jp/9784107723369.jpg" + # } + delete '/:version/books/:isbn' do + begin + token = params[:token] + isbn = params[:isbn] + id = @token.getId(token) + # 管理者権限でなければ利用できない + raise WebError.new(:status => 403, :code => "E13001", + :message => "User is without authority. userid=#{id}" + ) unless @user_account.checkRole(id, [User::ROLE_ADMIN]) + response = @book_manager.deleteBook(isbn) + json response + + rescue TokenManager::UnknownTokenError, + TokenManager::ExpiredTokenError + + # トークン認証失敗 + raise WebError.new(:status => 408, :code => "E14002", + :message => "Session expired of #{token}") + + rescue BookManager::NotFoundInstanceError + + # 対象の書籍が存在しない + raise WebError.new(:status => 404, :code => "E14002", + :message => "Not found target book. isbn=#{isbn}") + + end + end + # エラー時のレスポンス生成処理 error WebError do e = env['sinatra.error'] + logger.error("Failed Access error. #{e.params}") status e.params[:status] - response = {:code => e.params[:code]} + response = {:code => e.params[:code], :message => e.params[:message]} json response end end diff --git a/sinatra/app/controllers/spa_gui.rb b/sinatra/app/controllers/spa_gui.rb index 28eb7ad..ec09a3e 100644 --- a/sinatra/app/controllers/spa_gui.rb +++ b/sinatra/app/controllers/spa_gui.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true # SPA用コントローラ -# @auth OHASHI, Norikazu +# @author OHASHI, Norikazu require 'rack/cache' require 'sinatra/base' @@ -10,6 +10,7 @@ require 'sinatra' require 'haml' require 'logger' +# 書籍管理サーバ SPAGUI Controller class SPAGui < Sinatra::Base set :root, File.join(File.dirname(__FILE__), '..') set :views, proc { File.join(root, 'views') } diff --git a/sinatra/app/controllers/web_gui.rb b/sinatra/app/controllers/web_gui.rb index 66fca8a..74710d5 100644 --- a/sinatra/app/controllers/web_gui.rb +++ b/sinatra/app/controllers/web_gui.rb @@ -120,17 +120,17 @@ class WebGui < Sinatra::Base end end - # スタイルシート - get '/css/style.css' do - cache_control :public, :must_revalidate, :max_age => 120 - scss :'scss/style' - end - - # javaScript - get '/scripts/check_form.js' do - cache_control :public, :must_revalidate, :max_age => 120 - coffee :'coffee/check_form' - end + # # スタイルシート + # get '/css/style.css' do + # cache_control :public, :must_revalidate, :max_age => 120 + # scss :'scss/style' + # end + + # # javaScript + # get '/scripts/check_form.js' do + # cache_control :public, :must_revalidate, :max_age => 120 + # coffee :'coffee/check_form' + # end # メインページ get '/' do diff --git a/sinatra/app/models/books_db.rb b/sinatra/app/models/books_db.rb index 8f186e1..9d59ac7 100644 --- a/sinatra/app/models/books_db.rb +++ b/sinatra/app/models/books_db.rb @@ -65,7 +65,7 @@ class BookFull < ActiveRecord::Base end end -# ユーザ管理 +# 書籍情報管理 class BookManager include Singleton @@ -494,6 +494,66 @@ class BookManager end end + # 書籍一覧取得 + # @param [Boolean] nobody 所有者無しのみを検索 + def getBookList(nobody=false) + book_list = Book.all + if nobody + # BookCollection に該当 ISBN が含まれていない書籍だけを選別 + books = [] + book_list.each do |book| + book_collects = BookCollection.where(:isbn => book.isbn) + if (book_collects.empty?) + books.push(book) + end + end + return books + else + return book_list + end + end + + def createCollectBook(id, book) + isbn = book[:isbn] + book_info, summary, book_rank, cover_data = makeBookInfo(book) + + Book.transcation do + if isBook(isbn) + book_info.delete(:isbn) + updateBook(isbn, book_info) + else + createBook(book_info) + end + if !cover_data.nil? + BookCover.transcation do + cover_image = setBookImage(cover_data) + image_hash = addBookCover(isbn, cover_image) + cover_uri = "/book_image/#{image_hash}" + updateBook(isbn, :cover_uri => cover_uri) + end + end + createBookCollect(isbn, id, summary, book_rank) + end + end + + def updateCollectBook(id, book) + isbn = book[:isbn] + book_info, summary, book_rank, cover_data = makeBookInfo(book) + + Book.transcation do + updateBook(isbn, book_info) + if !cover_data.nil? + BookCover.transcation do + cover_image = setBookImage(cover_data) + image_hash = addBookCover(isbn, cover_image) + cover_uri = "/book_image/#{image_hash}" + updateBook(isbn, :cover_uri => cover_uri) + end + end + updateBookCollect(isbn, id, summary, book_rank) + end + end + private # ISBNの文字列から "-" を削除 # @param [String] isbn 処理対象のISBN @@ -777,4 +837,32 @@ class BookManager :book_rank => book_collect[:book_rank] } end + + # 更新用の書籍情報を作成する。 + # @param [Hash] params Postで取得した全パラメータ + # @return [Hash] 更新用書籍情報 + def makeBookInfo(book) + book_info = {} + book.each do |key, value| + case key + when 'summary', 'book_rank', 'cover_data' then + # 対象キーは書籍情報ではないので飛す + next + end + book_info[key.to_sym] = value + end + return book_info, book[:summary], book[:book_runk], book[:cover_data] + end + + # 書影情報の作成 + # @param [String] cover_data 書影データのData URI scheme(bas64) + # @return [Hash] 作成した書影データ + def setBookImage(cover_data) + cover_image = {} + # 書影情報の取得 + data_uri = URI::Data.new(cover_data) + cover_image[:mime_type] = data_uri.content_type + cover_image[:data] = data_uri.data + return cover_image + end end diff --git a/sinatra/app/models/tokens_db.rb b/sinatra/app/models/tokens_db.rb index cdc217b..001644d 100644 --- a/sinatra/app/models/tokens_db.rb +++ b/sinatra/app/models/tokens_db.rb @@ -14,16 +14,20 @@ ActiveRecord::Base.establish_connection(:development) # トークン情報 class Token < ActiveRecord::Base + + # トークンが期限切れかを確認する。 def expired? self.deadline < Time.now end + # トークン切れのセッション一覧を取得する。 def self.expired_items all.select {|d| d.deadline < Time.now} end end +# セッショントークン管理 class TokenManager include Singleton @@ -77,7 +81,7 @@ class TokenManager # @param [String] token 確認対象のトークン # @return [Integer] トークンを所持するユーザID # @raise [UnknownTokenError] 対象のトークンを保持していない - # @raise [ExpiredTokenError] 対処のトークンが期限切れ + # @raise [ExpiredTokenError] 対象のトークンが期限切れ def getId(token) begin token_item = Token.find_by(:token => token) diff --git a/sinatra/app/models/users_db.rb b/sinatra/app/models/users_db.rb index c5e82b3..c6580c3 100644 --- a/sinatra/app/models/users_db.rb +++ b/sinatra/app/models/users_db.rb @@ -178,9 +178,7 @@ class UserAccount end # ユーザ一覧の取得 - # @param [Integer] id 取得対象ユーザID # @return [Array] 全ユーザ情報の一覧 - # @raise [AuthenticationError] ユーザ権限不正 def getUserList return User.all end diff --git a/sinatra/app/views/layout.haml b/sinatra/app/views/layout.haml index c11f00b..61494d5 100644 --- a/sinatra/app/views/layout.haml +++ b/sinatra/app/views/layout.haml @@ -15,14 +15,16 @@ %body - if user_name.nil? - #main1= yield + #main1 + != yield - else #head #{user_name} 蔵書管理 Home #center #sidebar - = haml :sidebar - #main2= yield + != haml :sidebar + #main2 + != yield #foot %p 連絡先: -- 2.19.2