Skip to content

10fu3/StackCMS

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

README

japanese

コンテンツ管理システムの実装

1. はじめに

我々はヘッドレスコンテンツ管理システムの実現を目標にWebアプリケーションを作成した.

そもそもCMSとは、コンテンツ(画像や文章)を作成・更新・削除・読み取りできる機能を備えたソフトウェアである.

インターネット上に存在するWebページの半分がWordPressというコンテンツ管理システム(Content Management System:以下,CMS)によって作られていると言われている[1]. WordPressは

  • すでに他の誰かが開発したプラグインを有効化するだけで基本の機能を拡張できること
  • プラグインを開発する側になれば自分の理想となる機能を実装できる

という点で多くのWeb関係者に人気がある.

一方で

  • フロントエンド(ブラウザで表示する画面そのものの実装)とバックエンド(コンテンツ管理機能やそれを動かすためのデータベース実装)などを分離しづらいために同時並行での開発作業がやりづらい
  • 導入したプラグイン同士での機能の競合による不具合を自分で解決する必要がある
  • シェア数が多く, ハッカーの標的になりやすい

などのデメリットも抱えている.

WordPressは通常, ブラウザからのリクエストごとにコンテンツ管理システム上で動いているデータベースを叩き, 用意されたマークアップにコンテンツの内容を埋め込んでブラウザに返却する. サーバー側でWebページを描画するため, これを Server Side Rendering (SSR)方式という.

一方現在のWeb開発のトレンドとして, JamStackというものがある. これはWebサイトを構築するアーキテクチャの一種で,

  • JavaScript (画面を構成するためのロジックを記述する)
  • API (CMSとやり取りするための仕組み)
  • Markup (テンプレート化されたマークアップ)

(それぞれの頭文字をとってJam)という技術で成り立っている.

JamStackで構成されたWebページは

  • CMSの内容をCMSが用意したWEB APIを使って、HTML生成システムが事前にWebページを生成する
  • 生成したWebページをContentsDeliveryNetwork(以下,CDN:素早くWebコンテンツを取得できるWebキャッシュサービス)にキャッシュする
  • ブラウザからのリクエストに対応するのはCDNかつ, ブラウザに返却するコンテンツはすべて静的なので, 高速かつ安全にWebページを閲覧する仕組みを整えられる

という特徴がある.

このため, 従来のCMSの問題を解決したJamStack技術でWebページを作成するにはコンテンツ管理機能とフロントエンドの機能が分離しているCMSを用意する必要があり, この分離したCMSをヘッドレスCMSという.

そして今回はこのヘッドレスCMSを実装/用意した.

2. 目標

ヘッドレスCMSに要求される機能として,

  • コンテンツそのものの定義(ブログに求められる項目とコメントに求められる項目, 著者情報に求められる項目はそれぞれバラバラかつ, Webサイトによって変わってくるために, あとから自分で定義できることが必要)
  • 標準的なコンテンツの追加削除 (これはコンテンツ管理システムとして必須機能)
  • コンテンツの作成更新等を行うアカウントの管理と認証
  • ロールベースの権限認可機能
  • 上記システムをWebページ上で管理するためのRESTful WebAPI をそれぞれ実装する必要がある.

3. 考えたシステム

このシステムには次の用語が登場する.

  • コンテンツ これはこのシステムで管理される情報の集合を指す. 例えば,
    • ブログ記事
    • ブログのタグ
    • 著者情報

などを指す.

  • API Application Programming Interfaceの略. このシステムでは, 管理しているコンテンツをRESTfulなWEB APIを通じて操作, 取得するために, エンドポイントを定義する必要がある.

  • アカウント(ユーザー) このシステムでの管理者/利用者を抽象的に表現したものである.

  • 権限 このシステムで機能を利用するための権利を権限とし, 各機能に割り当てられている.

  • ロール アカウントの集合であると同時に権限の集合. ロールに紐付いているアカウントは同じロールに紐付いている権限を保有しているものとしてサービスで扱われる. (RBAC, ロールベースアクセス制御)

このシステムではコンテンツのCRUDを提供する. またコンテンツの項目を定義することが可能で, あとからコンテンツの定義を更新することができる. このコンテンツ情報そのものの保管はNoSQLであるMongoDBで実装した. これらのコンテンツはRESTfulなWEB APIを通してCRUDをするためにエンドポイント(名)を自分で設定する必要がある.

またシステムの利用者を識別するためにユーザーを事前に管理者アカウントを使って定義し, ユーザーアカウントを作成しておく. そのアカウントを使ってコンテンツのCRUDを行う. ユーザーアカウントにはログイン・ログアウトの機能があり, ログイン中はCURDのHTTPリクエストにAuthorizationヘッダをもたせて, セッションキーを各リクエスト時に同時に送信する.

次の表1に示すエンドポイントをフロントエンドアプリケーション(Javascript製)からリクエストを送信することで各操作を実現する.

CRUDをするためのRESTful WEB API エンドポイント一覧

RESTの種類 エンドポイント名 パスパラメーター 役割
GET (取得) /api/v1/auth/me なし ログイン状態を返す, ログインしていればプロフィール情報がJSONで返却される
POST (作成) /api/v1/auth/login なし メールアドレスとパスワードを受け付ける, ログインできればセッションキーとプロフィール情報がJSONで返却される
POST (作成) /api/v1/auth/logout なし セッションキーを受け付ける. システム上からセッションキーが破棄される
POST (作成) /api/v1/contents/:api_id api_id: API固有のID 定義されたAPIにコンテンツを投稿する
PATCH (更新) /api/v1/contents/:api_id/:content_id api_id: API固有のID 定義されたAPIに投稿されたコンテンツのうち, コンテンツ固有のIDをもつコンテンツを更新する
GET (取得) /api/v1/contents/:api_id api_id: API固有のID 定義されたAPIに投稿されたコンテンツを取得する. クエリパラメータで条件を設定し, 取得項目にフィルターできる.
DELETE (削除) /api/v1/contents/:api_id/all api_id: API固有のID API固有のIDをもつAPIに所属するコンテンツをすべて削除する
DELETE (削除) /api/v1/contents/:api_id/:content_id api_id: API固有のID,content_id: コンテンツ固有のID 指定したAPIに所属する指定したコンテンツIDをもつコンテンツを削除する
GET (取得) /api/v1/meta/:api_id api_id: API固有のID 定義されたAPIに投稿されたコンテンツのメタ情報(投稿日時などのコンテンツ情報を含まない情報)を取得する. クエリパラメータで条件を設定し, 取得項目にフィルターできる.
PATCH (更新) /api/v1/meta/:api_id/:content_id/status api_id: API固有のID, content_id: コンテンツ固有のID コンテンツ固有のIDの公開状態を更新する(公開 or 非公開)
POST (作成) /api/v1/define API定義のリクエストを受け付ける
PATCH (更新) /api/v1/define/:api_id api_id: API固有のID 指定したAPI固有のIDを持つAPIの情報(エンドポイント名, コンテンツ定義)を更新する
GET (取得) /api/v1/define/:api_id api_id: API固有のID 指定したAPI固有のIDを持つAPIの情報(エンドポイント名, コンテンツ定義)を取得する
GET (取得) /api/v1/define/all 定義されたAPIの情報(エンドポイント名, コンテンツ定義)をすべて取得する
DELETE (削除) /api/v1/define/:api_id api_id: API固有のID 指定したAPI固有のIDを持つAPIを削除する
POST (作成) /api/v1/user ユーザー登録のリクエストを受け付ける
PATCH (更新) /api/v1/user/:user_id user_id: ユーザー固有のID 指定されたユーザーIDに該当するユーザー情報(プロフィール, パスワード)を更新する
GET (取得) /api/v1/user 登録されたすべてのユーザー情報を送信する
GET (取得) /api/v1/user/:user_id user_id: ユーザー固有のID 指定されたユーザーIDに該当するユーザー情報(ハッシュ化されたパスワード以外)を取得する
DELETE (削除) /api/v1/user/:user_id user_id: ユーザー固有のID 指定されたユーザーIDに該当するユーザーを削除する
POST (作成) /api/v1/role ロール登録のリクエストを受け付ける
PATCH (更新) /api/v1/role/:role_id role_id: ロール固有のID 指定されたロールIDに該当するロールを更新する
DELETE (削除) /api/v1/role/:role_id role_id: ロール固有のID パラメーターで指定されたロール固有のID
GET (取得) /api/v1/role/all 登録されたロールをすべて取得する

4. 作成したテーブル

  • ユーザーテーブル, ユーザー情報(メールアドレス,パスワード, プロフィール)を管理する
usersテーブル 役割
(PK) user_id VARCHAR(40) ユーザー固有のID
nick_name VARCHAR(128) ニックネーム
(PK) mail VARCHAR(256) メールアドレス
password_hash VARCHAR(512) ハッシュ化したパスワード
is_lock BOOLEAN 更新不可能フラグ, 管理者アカウントのみ有効
  • ログインセッションテーブル, ログインしているユーザーを識別するセッションキー
login_sessionテーブル 役割
(PK) session_id VARCHAR(40) セッションキー
user_id VARCHAR(40) ログインしているユーザーの固有ID
expired_at DATETIME 有効期限, ログイン時にセッションキーの有効期限が現在時刻を超えている場合, 破棄される
  • ロールテーブル, ロールそのものを定義するテーブル
rolesテーブル 役割
(PK) role_id VARCHAR(40) ロール固有のID
role_name VARCHAR(512) ロール名
is_lock BOOLEAN 更新不可能フラグ, 管理者ロールのみ有効
  • ユーザーロールテーブル, ユーザーとロールを関連付ける中間テーブル
user_roleテーブル 役割
(PK) user_role_id VARCHAR(80) ユーザーロール識別用カラム, user_idとrole_idを結合したものを格納することで, 重複した関係レコードを登録しないようにしている
user_id VARCHAR(40) ユーザー固有のID
role_id VARCHAR(40) ロール固有のID
  • ロールアビリティーテーブル, ロールと権限を関連付けるテーブル
role_abilityテーブル 役割
(PK) role_ability_id VARCHAR(80) 固有のID
role_id VARCHAR(40) 固有のID
ability_id VARCHAR(512) 固有のID
  • APIテーブル, コンテンツの集合でもあり, コンテンツ管理用APIのエンドポイントとして機能するための情報を管理する
apisテーブル 役割
(PK) id VARCHAR(40) API固有のID
api_id VARCHAR(40) RESTful WEB APIのエンドポイント名
is_single BOOLEAN 扱うコンテンツが単数か複数かを表す
  • フィールドテーブル, コンテンツが持つ属性を定義する (例えばblogAPIを用意する場合, コンテンツにはtitle:文字列, body:文字列という属性が必要になるため, api_idにはblogAPIが入って, 各レコードにtitleとbodyが登録される)
fieldsテーブル 役割
(PK) field_id VARCHAR(40) フィールド固有のID
api_id VARCHAR(40) API固有のID,APIに紐付けられている
field_name VARCHAR(40) フィールド名
field_type VARCHAR(40) フィールドのデータ型(文字列,数字,日付,参照)
relation_api VARCHAR(40) フィールドが他のAPIのコンテンツを参照する場合, そのAPIの固有IDを指定する
  • クライアントテーブル BOTやページジェネレーターがコンテンツを取得するためにクライアントとして登録する
clientsテーブル 役割
(PK) client_id VARCHAR(80) クライント固有のID
api_id VARCHAR(80) API固有のID,APIに紐付けられている
client_name VARCHAR(80) クライアント名
client_secret VARCHAR(512) クライアントシークレット, クライアントからリクエストを飛ばすときはクライアントシークレットをAuthorizationヘッダに持たせる
  • コンテンツテーブル コンテンツのメタ情報を登録する
contentsテーブル 役割
(PK) content_id VARCHAR(40) コンテンツ固有のID
api_id VARCHAR(40) API固有のID, APIに紐付けられている
created_at DATETIME コンテンツ作成時刻
updated_at DATETIME コンテンツ更新時刻
published_at DATETIME コンテンツ公開時刻
revised_at DATETIME コンテンツ改定時刻
publish_will DATETIME コンテンツ公開予定時刻
stop_will DATETIME コンテンツ公開停止時刻

5. 機能を実現するSQL文と実行結果

CRUDごとにSQL文を示す.

? の部分はWebアプリケーションが変数を埋め込むためのプリペアードステートメントである.

ユーザーセッションテーブルのCURD

  • C
INSERT INTO login_session values (?,?,?)
  • U 実装上アップデートすることがないのでなし
  • R セッションキー検索時
SELECT * FROM login_session WHERE session_id = ?
  • D セッションキーを破棄する
DELETE FROM login_session where session_id = ?

ユーザー削除時

DELETE FROM login_session where user_id = ?

コンテンツテーブルのCURD

  • C
INSERT INTO contents(content_id,api_id,created_at,updated_at,published_at,revised_at,created_by,updated_by,publish_will,stop_will) VALUES(?,?,?,?,?,?,?,?,?,?)
  • U
UPDATE contents SET update_by = ?, update_at = ?, published_at = ? WHERE content_id = ?
  • R すべてのコンテンツ(下書きを含む)検索時
SELECT * FROM contents WHERE content_id IN (?) order by created_at

公開されたコンテンツのみ検索時検索時

SELECT * FROM contents WHERE content_id IN (?) and published_at is not null order by published_at desc
  • D コンテンツ削除時
DELETE FROM contents WHERE content_id = ?

API削除時

DELETE FROM contents WHERE api_id = ?

ユーザーテーブルのCURD

  • C
INSERT INTO users (user_id,nick_name,mail,password_hash,is_lock) VALUES(?,?,?,?,?)
  • U
UPDATE users SET password_hash = ?, nick_name = ?, mail = ? WHERE user_id = ? AND is_lock = false
  • R

メールアドレス検索時

SELECT * FROM users WHERE mail = ?

全件検索時

SELECT * FROM users
  • D
DELETE FROM users WHERE user_id = ? AND is_lock = false

APIテーブルのCURD

  • C
INSERT INTO apis (id,api_id,is_single) VALUES(?,?,?)
  • U
UPDATE apis SET api_id = ?, is_single = ? WHERE id = ?
  • R API検索時
SELECT * FROM apis WHERE id = ?

全件取得時

SELECT * FROM apis
  • D
DELETE FROM apis WHERE id = ?

フィールドテーブルのCURD

  • C
INSERT INTO fields (field_id,api_id,field_name,field_type,relation_api) VALUES(?,?,?,?,?)
  • U なし, 更新時はすべてフィールドは削除したうえでもう一度作成されなおす
  • R
SELECT * FROM fields WHERE api_id = ?
  • D
DELETE FROM fields WHERE field_id = ? AND api_id = ?

APIごと削除された場合

DELETE FROM fields WHERE api_id = ?

参照フィールドのうち, 参照先のAPIが削除された場合

DELETE FROM fields WHERE relation_api = ?

ロールテーブルのCURD

  • C
INSERT INTO roles (role_id,role_name,is_lock) VALUES (?,?,?)
  • U
UPDATE roles SET role_name = ? WHERE role_id = ? AND is_lock = false
  • R ロール検索時
SELECT * FROM roles WHERE role_id = ?

全件取得時

SELECT * FROM roles
  • D
DELETE FROM roles WHERE role_id = ? AND is_lock = false

ユーザーロールテーブルのCURD

  • C
INSERT INTO user_role (user_role_id,user_id,role_id) VALUES (?,?,?)
  • U

実装上, CRDのみ必要なためなし

  • R
SELECT * FROM roles WHERE role_id IN (SELECT role_id FROM user_role WHERE user_id = ?)
  • D

ユーザーの削除時

DELETE FROM user_role WHERE user_id = ?

ロールの削除時

DELETE FROM user_role WHERE role_id = ?
  • その他

    ユーザーAとユーザーBが同じロールに属しているかどうか調べる

    SELECT * FROM roles WHERE role_id IN (SELECT role_id FROM user_role WHERE user_id = ? AND role_id IN (SELECT role_id FROM user_role WHERE user_id = ?))

    特定のユーザーが特定の権限を持っているかを調べる

    select * from role_ability inner join user_role on role_ability.role_id = role_ability.role_id where ser_id = ? and ability_id IN (?)

6. 実行画面/画面遷移

本画像では, ブログ用のCMSを構築する際に必要となる設定をスクリーンショットしたものである

ブログ用のCMSには次のAPIとフィールドを定義した

API名:API内容 author:筆者を管理する

フィールド名 フィールドの用途 データ型
name 筆者の名前 文字列
introduction 筆者の自己紹介 文字列
icon ブログに表示される 文字列

blog:ブログに必要な情報を管理する

フィールド名 フィールドの用途 データ型
title 記事のタイトル 文字列
body 記事本文 文字列
author 著者紹介 参照型(authorAPIを参照する)
related_blog 関連記事 参照型(blogAPIを参照する)
  1. ログイン画面

  2. ホーム画面

  3. API作成画面

  4. コンテンツ新規投稿画面

  5. 同様にコンテンツをもう一つ追加した場合

  6. APIを更に追加 (ブログ本文用のAPIを作成)

  7. 自分自身を参照するフィールドを追加, APIを更新する (関連記事を参照させるためのフィールドを定義する) (related_blogのフィールドが追加された)

  8. blogAPIに記事を投稿する (参照型のフィールドを持つAPIの場合, 次のように参照先のAPIの画面が表示されて, 参照先APIのコンテンツを登録できるようになる) 作成すると次の表示になる

  9. コンテンツの編集 次のようにコンテンツ編集画面が表示され, コンテンツの編集と公開状態の設定, 削除などができる 公開状態を変更すると次の表示になる

  10. ユーザーの新規登録

  11. ユーザー一覧の表示

  12. ユーザーのプロフィール表示

  13. ロールの新規登録

  14. ロールの権限表示

  15. ロールの権限更新 今回はコンテンツの投稿権限を与えるようにロールを更新する. ロールの権限一覧画面から ロール編集画面を開き 更新する権限を選択する 適用を押して ロールの権限が更新されたことがわかる

  16. コンテンツの取得 (WEB API) 今回はRESTfulAPI を使ってサイトジェネレーターに本CMSのAPIを利用するために, 便宜上ブラウザのRESTfulAPIを叩くためのツールを使って実行する. このように違うセッションキーが入るとエラーが帰ってくる 正しいセッションキーを入れて, APIを実行すると, 下の図のようにCMSに投稿したコンテンツが表示される あとはこの構造化されたデーターをサイトジェネレーターが読み込んで, WEBページを作成する

  17. 参考文献 [1] WordPress powers 25% of all websites - The market share among the 300+ content management system which we monitor is now at 58.7% より https://w3techs.com/blog/entry/wordpress-powers-25-percent-of-all-websites [2] 業務システムにおけるロールベースアクセス制御 - RBACの基礎 https://qiita.com/kawasima/items/8dd7eda743f2fdcad78e [3] 権限制御の仕組み[ロールベースアクセスコントロール(RBAC)] - ロールベースアクセスコントロールrbachttps://www.bnote.net/dataroom/rbac.html [4] ロールベースアクセス制御(RBAC)とは? メリットとABACとの比較 - ロールベースアクセス制御(RBAC)とは具体的に何か? https://www.okta.com/jp/identity-101/what-is-role-based-access-control-rbac/

english:

Headless CMS software

This CMS can...

  • define your original 'contents' field
  • define relation about your 'contents' to your 'contents'
  • search contents by 'filter' (that is mongo db filter)

based on

  • golang
    • gin (web framework)
  • maria db (mysql)
    • contents field management
    • account management
    • permission management
    • role management
  • mongo db
    • contents maneged

feature

  • user login

  • content

    • create
    • update
    • read
    • delete
    • publish
  • api

    • create
    • read
    • update
    • delete
  • user

    • read
    • create
    • update
    • delete
  • role

    • read
    • create
    • update
    • delete

not implement

  • webhook

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages