何かの時に必要になるかもしれないと思い、 Common Lisp で Web アプリを作るためのブランクプロジェクトを作ってみた。
https://github.com/quek/hunchentoot-blank
Hunchentoot, CL-WHO, CLSQL を使ったブランクプロジェクト。
データベースには MySQL を使用。シェルから次のコマンドを実行してデータベースの作成が必要。
echo 'create database hunchentoot_blank default character set utf8;' | mysql -u root
次を実行して http://localhost:8888/ にアクセスする。
(require :hunchentoot-blank)
(hunchentoot-blank::start)
これだけではちょっと寂しいので、いつものようにソースを載せておく。
(in-package #:hunchentoot-blank)
(defparameter *default-directory*
(pathname (directory-namestring #.(or *compile-file-truename*
*load-truename*)))
"このファイルがあるディレクトリ")
(defparameter *js-path* (merge-pathnames "js/" *default-directory*)
"JavaScript 用ディレクトリ")
(defparameter *css-path* (merge-pathnames "css/" *default-directory*)
"スタイルシート用ディレクトリ")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; DB
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
#|
シェルから次のコマンドを実行してデータベースを作成してください。
echo 'create database hunchentoot_blank default character set utf8;' | mysql -u root
|#
(clsql-sys:file-enable-sql-reader-syntax)
(defparameter *connection-spec* '("localhost" "hunchentoot_blank" "root" "")
"MySQL の接続情報。(DBサーバ DB名 ユーザ パスワード)")
(defmacro with-db (&body body)
(alexandria:with-gensyms (res handler-done)
`(clsql:with-database (clsql:*default-database*
*connection-spec*
:make-default t
:pool t
:encoding :utf-8
:database-type :mysql)
;; for debug
(clsql-sys::start-sql-recording)
(unwind-protect
(let (,res (,handler-done t))
;; (clsql:execute-command "SET NAMES 'utf8'")
(clsql-sys:with-transaction (:database clsql:*default-database*)
;; hunchentoot:redirect した場合の対応
(catch 'hunchentoot::handler-done
(setf ,res (progn ,@body))
(setf ,handler-done nil)))
(if ,handler-done
(throw 'hunchentoot::handler-done nil)
,res))
;; for debug
(clsql-sys::stop-sql-recording)))))
;;(with-db (clsql-sys:query "select 'あ'"))
(clsql-sys:def-view-class user ()
((id :accessor id
:initarg :id
:db-kind :key
:db-constraints :auto-increment
:type integer)
(email :accessor email
:initarg :email
:db-constraints :unique
:type string)
(password :initarg :password
:initarg :plain-password
:type string)))
(defmethod initialize-instance :after ((user user)
&key plain-password
&allow-other-keys)
"make-instance で :plain-password が指定されていた場合、
password に hash-password したものを設定する。"
(when plain-password
(setf (password user) plain-password)))
(defun hash-password (password)
"パスワードのハッシュ関数"
(ironclad:byte-array-to-hex-string
(ironclad:digest-sequence
:sha256
(ironclad:ascii-string-to-byte-array password))))
(defmethod (setf password) (password (user user))
"パスワードのハッシュをセットする。"
(setf (slot-value user 'password) (hash-password password)))
(defun authenticate (email password)
(let ((password (hash-password password)))
(car
(clsql:select 'user
:flatp t
:where [and [= [email] email] [= [password] password]]))))
;; (authenticate "user1@example.com" "password")
;; テーブル作成
(with-db
(unless (clsql-sys:table-exists-p 'user)
(clsql-sys:create-view-from-class 'user)
'user))
#+テーブル削除
(progn
(clsql-sys:drop-view-from-class 'user))
#+テストデータ作成
(with-db
(let ((user (make-instance 'user :email "user1@example.com"
:plain-password "password")))
(clsql-sys:update-records-from-instance user)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Web server
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(setf
;; for utf-8
hunchentoot:*hunchentoot-default-external-format* (flexi-streams:make-external-format :utf-8)
hunchentoot:*default-content-type* "text/html; charset=utf-8"
;; for debug
hunchentoot:*catch-errors-p* nil)
(setf hunchentoot:*dispatch-table*
(list
'hunchentoot:dispatch-easy-handlers
(hunchentoot:create-folder-dispatcher-and-handler "/css/" *css-path*)
(hunchentoot:create-folder-dispatcher-and-handler "/js/" *js-path*)))
(defvar *acceptor*)
(defun start (&optional (port 8888))
"Web サーバ起動"
(setf *acceptor* (hunchentoot:start
(make-instance 'hunchentoot:acceptor
:port port))))
(defun stop ()
"Web サーバ停止"
(hunchentoot:stop *acceptor*))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ビュー
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defvar *login-user* nil "ログインユーザ")
(defun login (user redirect-url)
"ログイン処理"
(setf *login-user* user)
(when hunchentoot:*session*
(hunchentoot:remove-session hunchentoot:*session*))
(hunchentoot:start-session)
(setf (hunchentoot:session-value 'login-user-id) (id user))
(hunchentoot:redirect redirect-url))
(defun logout (redirect-url)
"ログアウト処理"
(when hunchentoot:*session*
(hunchentoot:remove-session hunchentoot:*session*))
(setf *login-user* nil)
(hunchentoot:redirect redirect-url))
(setf *prologue* "<!DOCTYPE html>")
(setf *attribute-quote-char* #\")
(defmacro with-default-template ((&key (title "題名")
(charset "UTF-8")) &body body)
"ページのテンプレート"
`(with-html-output-to-string (out nil :prologue t :indent t)
(htm (:html :lang "ja"
(:head
(:meta :charset ,charset)
(:title ,title)
(:link :rel "stylesheet" :href"css/main.css" :media "all")
(:script :src "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"))
(:body ,@body)))))
(defun select-login-user ()
(let ((login-user-id (hunchentoot:session-value 'login-user-id)))
(when login-user-id
(caar (clsql:select 'user
:where [= [id] login-user-id])))))
(defmacro with-login-user (&body body)
`(let ((*login-user* (select-login-user)))
,@body))
(defmacro define-page (description lambda-list &body body)
"ページ定義。
description
hunchentoot:define-easy-handler に加えて
ログインが必要な場合は :login-require-p t を指定する。
lambda-list
hunchentoot:define-easy-handler と同じ。"
(let ((login-required-p (and (listp description)
(getf (cdr description) :login-require-p))))
(when (listp description)
(remf (cdr description) :login-require-p))
`(hunchentoot:define-easy-handler ,description ,lambda-list
(with-db
(with-login-user
,@(when login-required-p
`((unless *login-user*
;; ログインしていな場合の処理
(hunchentoot:redirect "/"))))
,@body)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; 各ページ
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; トップページ
(define-page (%root :uri "/") ()
(with-default-template (:title "トップ")
(htm
(:div :class "ba" "ブランクプロジェクト")
(if *login-user*
(htm (:div (str (email *login-user*))
" でログインしています。")
(:div (:a :href "logout" "ログアウト")))
(htm (:div (:a :href "login" "ログイン"))))
(htm (:div (:a :href "/secret" "ログインが必要なページへのリンク"))))))
;;;; ログインページ
(define-page (%login :uri "/login") (email messages)
(with-default-template (:title "ログイン")
(htm
(when messages
(htm (:ul (loop for message in messages
do (htm (:li (str message)))))))
(:form :action "authenticate" :method :post
(:div "email"
(:input :type :text :name "email" :value email))
(:div "パスワード"
(:input :type :password :name "password"))
(:div (:input :type :submit :value "ログイン"))))))
;;;; 認証
(define-page (%authenticate :uri "/authenticate")
((email :init-form "") (password :init-form ""))
(let (messages)
(when (string= "" email)
(push "email を入力してください。" messages))
(when (string= "" password)
(push "パスワードを入力してください。" messages))
(if messages
(%login :email email :messages (reverse messages))
(let ((user (authenticate email password)))
(if user
(login user "/")
(%login :email email))))))
;;;; ログアウト
(define-page (%logout :uri "/logout") ()
(logout "/"))
;;;; ログインが必要なページ
(define-page (secrect :uri "/secret" :login-require-p t) ()
(with-default-template (:title "秘密のページ")
(htm (:p "このページはログインが必要なページです。"))))