#|

src/models/user.lisp

# User Interaction Backend

## Create a superuser

(create-superuser name email password)

## Create a normal user

    (bookshops.models::create-user "Joe Blogg" "JoeBlogg@example.com" "i<3books")

## Give him rights

    (add-role user :admin)

Bootstrap roles: see database.lisp bootstrap-base-roles.

# More Information

- <https://github.com/fukamachi/can>

|#

(in-package #:cl-deck-builder2.models.user)

(defclass user (has-secure-password)
  ((name :accessor name-of
         :col-type :text
         :initarg :name)
   (email :accessor user-email-of
          :col-type :text
          :initarg :email))
  (:unique-keys email)
  ;; (:primary-key email)
  (:metaclass registered-table-class)
  (:documentation "The USER class encapsulates information about the users of The Deck Builder: name, email, password, password salt, user roles."))

(defclass role ()
  ((name ;;:col-type :text
    :col-type (:varchar 64)
    :initarg :role
    :inflate (alexandria:compose #'alexandria:make-keyword #'string-upcase)
    :deflate #'string-downcase
    :accessor name-of))
  (:documentation "The ROLE class encapsulates information about the roles of actions a user may perform, e.g. :ADMIN, :USER, :BANNED.")
  (:metaclass registered-table-class)
  (:primary-key name)
  (:auto-pk nil)
  (:record-timestamps nil))

(defclass user-role ()
  ((user :col-type user
         :initarg :user
         :accessor user-role-user)
   (role :col-type role
         :initarg :role
         :accessor user-role-role))
  (:metaclass registered-table-class)
  (:primary-key user role)
  (:auto-pk nil)
  (:record-timestamps nil))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun create-user (name email password)
  "Create a USER object with NAME, EMAIL, and PASSWORD fields. Will automatically be synced to db via CREATE-DAO. Password hashing provided by MITO-AUTH."
  (create-dao 'user :name name
                    :email email
                    :password password))

(defun find-user (email)
  "Find a user by EMAIL."
  (find-dao 'user :email email))

;; (defun create-superuser (name email password)
;;   "Create a USER with the ADMIN role."
;;   (let ((user (create-user name email password)))
;;     (add-role user :admin)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TODO Role Viewer / Editor
(defun create-role (name)
  "Create a ROLE called NAME. If the ROLE exists, return that, instead."
  (with-connection (db)
    (with-transaction
      (or (mito:find-dao 'role :name name)
          (mito:create-dao 'role :name name)))))

(defun %find-role-from-keyword (role-name)
  "Return a role object from a keyword and throw an error if an associated role cannot be found"
  (let ((role (find-dao 'role :name role-name)))
    (assert role (role) "There is no role named ~a" role-name)
    role))

(defgeneric add-role (user role)
  (:documentation "Add the given role to this user. ROLE is either a role object or a symbol. An example role is ':admin`.")
  (:method ((user user) (role role))
    (or (find-dao 'user-role :user user :role role)
        (create-dao 'user-role :user user :role role)))
  (:method ((user user) (role-name symbol))
    (let ((role (%find-role-from-keyword role-name)))
      (add-role user role))))

;; Retrieving roles from "user-role" table
(defmethod user-roles-of ((user user))
  (with-connection (db)
    (with-transaction
      (mapcar #'user-role-role
              (mito:retrieve-dao 'user-role :user user)))))

(defgeneric user-has-role (user role)
  (:documentation "Given a USER and a ROLE, return T if  ROLE is a MEMBER of USER-ROLES for USER, NIL otherwise.")
  (:method ((user user) (role role))
    (member (name-of role) (user-roles-of user) :key #'name-of))
  (:method ((user user) (role-name symbol))
    (let ((role (%find-role-from-keyword role-name)))
      (user-has-role user role))))

(defmethod user-admin-p ((user user))
  "Is USER an admin? Return T if so. NIL otherwise."
  (user-has-role user :admin))

(defmethod user-verified-p ((user user))
  "Is USER in the VERIFIED role? Return T if so. NIL otherwise."
  (user-has-role user :verified))
