guideExpress

This guide describes the integration of CKBox in an Express application. If you prefer to jump straight to the code, you can find the complete code of the application described in this guide on GitHub.

# Prerequisites

Before we start, please ensure that you have a recent LTS version of Node.js installed in your system. If the tool is not available from the command line, please follow the Node.js download instructions first.

# Creating the application

As a base in our example, we will use a project created with Express project generator. To start, let’s create a directory for the app, enter it, and use the following command:

npx express-generator --view=ejs --git

After the project is created, update the list of dependencies and install packages:

npx ncu -u && npm install

Finally, we can remove routes/users.js and public/stylesheets/style.css files from the template since we won’t be using them.

# Environment variables

The app we are building relies on a few variables that would be better kept secret. Therefore, before jumping onto the next steps, let’s create a .env file in the project’s root. Over the course of this guide, we will be adding our envrionment variables there.

Let’s install the dotenv package that will help us use the .env file in the project:

npm add dotenv

# Authentication

In a typical scenario access to CKBox will be restricted to authenticated users only. Therefore, we will introduce a simple authentication mechanism with the help of Passport to our app. Let’s start by installing the required dependencies. For simplicity, we are omitting CSRF protection measures in the app. Please note this example uses a simplified approach that should not necessarily be used in your production application.

npm add passport passport-local express-session connect-ensure-login

Then, let’s create a routes/auth.js router that will take care of user authentication. We will be using 3 test users and each one of them will be assigned a different CKBox role: user, admin, or superadmin. Passport’s method passport.serializeUser will make sure that the user’s info, including their role in CKBox, will be made available for other route handlers.

Below you can find the complete code for handlers within the authentication router.

// routes/auth.js

const express = require("express");
const passport = require("passport");
const LocalStrategy = require("passport-local");

const dbUsers = [
    {
        id: "1",
        role: "user",
        email: "user@acme.com",
        password: "testpwd123",
        name: "John",
    },
    {
        id: "2",
        role: "admin",
        email: "admin@acme.com",
        password: "testpwd123",
        name: "Joe",
    },
    {
        id: "3",
        role: "superadmin",
        email: "superadmin@acme.com",
        password: "testpwd123",
        name: "Alice",
    },
];

passport.use(
    new LocalStrategy({ usernameField: "email" }, function (email, password, done) {
        const dbUser = dbUsers.find(function (dbUser) {
            return dbUser.email === email && dbUser.password === password;
        });

        if (!dbUser) {
            return done(null, false);
        }

        return done(null, dbUser);
    })
);

passport.serializeUser(function (user, cb) {
    process.nextTick(function () {
        cb(null, { id: user.id, name: user.name, role: user.role });
    });
});

passport.deserializeUser(function (user, cb) {
    process.nextTick(function () {
        return cb(null, user);
    });
});

const router = express.Router();

router.get("/login", function (req, res, next) {
    res.render("login");
});

router.post(
    "/login/password",
    passport.authenticate("local", {
        successReturnToOrRedirect: "/",
        failureRedirect: "/login",
        failureMessage: true,
    })
);

router.post("/logout", function (req, res, next) {
    req.logout(function (err) {
        if (err) {
            return next(err);
        }

        res.redirect("/");
    });
});

module.exports = router;

# Token URL

CKBox, like other CKEditor Cloud Services, uses JWT tokens for authentication and authorization. All these tokens are generated on your application side and signed with a shared secret that you can obtain in the CKEditor Ecosystem dashboard. For information on how to create access credentials, please refer to the Creating access credentials article in the Authentication guide.

Now that we have the required access credentials, namely: the environment ID and the access key, let’s create the token endpoint. As a base, we will use the code of the generic token endpoint in Node.js.

First, let’s install the jsonwebtoken library for creating JWT tokens:

npm add jsonwebtoken

Let’s wrap the logic of the Node.js token endpoint with the Express route handler and generate a token with the payload required by CKBox. Please note that access to this endpoint is limited to authenticated users only.

Let’s create an Express router that will handle all requests related to CKBox. We will rely on the req.user object set by Passport for authenticated users.

// routes/ckbox.js

const express = require("express");
const jwt = require("jsonwebtoken");

const CKBOX_ENVIRONMENT_ID = process.env.CKBOX_ENVIRONMENT_ID;
const CKBOX_ACCESS_KEY = process.env.CKBOX_ACCESS_KEY;

const router = express.Router();

router.get("/api/ckbox", function (req, res, next) {
    const user = req.user;

    if (user && user.role) {
        const payload = {
            aud: CKBOX_ENVIRONMENT_ID,
            sub: user.id,
            auth: {
                ckbox: {
                    role: user.role,
                },
            },
        };

        const result = jwt.sign(payload, CKBOX_ACCESS_KEY, {
            algorithm: "HS256",
            expiresIn: "1h",
        });

        res.send(result);
    } else {
        next({ message: "Unauthenticated user", status: 401 });
    }
});

module.exports = router;

As you can see on the code listing above, the access credentials required to sign JWT tokens are obtained from the environment variables. Thanks to this, you can conveniently add them to the .env file:

# .env
CKBOX_ENVIRONMENT_ID=REPLACE-WITH-ENVIRONMENT-ID
CKBOX_ACCESS_KEY=REPLACE-WITH-ACCESS-KEY

# Views

Now, let’s build a couple of views. We will be using Tailwind CSS to bring an appealing look to our example app. For simplicity, we will be using the Play CDN to use Tailwind right in the browser without any build steps. Please note that this approach is not the best choice for production.

# Login view

Let’s start by creating a simple login view under views/login.ejs. Users will be providing their email addresses and passwords, and we will be using /login/password handler created before to authenticate them.

<!-- views/login.ejs -->

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <script src="https://cdn.tailwindcss.com"></script>
        <title>Login</title>
    </head>
    <body>
        <section class="flex justify-center items-center h-screen">
            <form
                action="/login/password"
                method="post"
                class="bg-blue-500 text-center w-1/2 xl:w-1/3 px-3 py-4 rounded flex flex-col gap-2"
            >
                <section class="flex flex-col gap-1">
                    <label for="email" class="text-left text-white">Email</label>
                    <input
                        id="email"
                        name="email"
                        type="text"
                        required
                        autofocus
                        class="block w-full mx-auto text-sm py-2 px-3 rounded"
                    />
                </section>
                <section class="flex flex-col gap-1">
                    <label for="password" class="text-left text-white">Password</label>
                    <input
                        id="current-password"
                        name="password"
                        type="password"
                        required
                        class="block w-full mx-auto text-sm py-2 px-3 rounded my-3"
                    />
                </section>
                <button
                    type="submit"
                    class="bg-blue text-white font-bold py-2 px-4 rounded border block mx-auto w-full"
                >
                    Login
                </button>
            </form>
        </section>
    </body>
</html>

# CKBox with CKEditor

In the views/ckeditor.ejs view, we will be using the quickest and easiest way to run CKEditor 5 which uses a predefined build served from the CDN. For more advanced integration scenarios, please refer to the CKEditor 5 documentation.

<!-- views/ckeditor.ejs -->

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <script src="https://cdn.tailwindcss.com"></script>
        <script src="https://cdn.ckbox.io/ckbox/2.4.0/ckbox.js"></script>
        <script src="https://cdn.ckeditor.com/ckeditor5/41.3.0/classic/ckeditor.js"></script>
        <link
            rel="stylesheet"
            href="https://cdn.ckbox.io/ckbox/2.4.0/styles/themes/lark.css"
        />
        <title>CKEditor</title>
        <style>
            .ck-editor__editable_inline {
                min-height: 400px;
            }
        </style>
    </head>
    <body>
        <div class="mx-auto h-screen flex flex-col">
            <%- include('nav'); %>
            <div class="px-8 flex-1 bg-accents-0">
                <main class="w-full max-w-4xl mx-auto py-16 h-full flex flex-col">
                    <section class="flex flex-col gap-6">
                        <h2 class="text-4xl font-semibold tracking-tight">CKEditor</h2>
                    </section>
                    <hr class="border-t border-accents-2 my-6" />
                    <section class="flex flex-col gap-3 h-4/5">
                        In this example CKBox is integrated with CKEditor. With CKBox plugin,
                        CKEditor will upload files directly to your CKBox environment. Use icon in
                        the top-left corner of the editor to open CKBox as a file picker.
                        <div id="editor"></div>
                    </section>
                </main>
            </div>
        </div>
        <script>
            ClassicEditor.create(document.querySelector("#editor"), {
                ckbox: {
                    tokenUrl: "<%= tokenUrl %>",
                    theme: "lark",
                },
                toolbar: [
                    "ckbox",
                    "imageUpload",
                    "|",
                    "heading",
                    "|",
                    "undo",
                    "redo",
                    "|",
                    "bold",
                    "italic",
                    "|",
                    "blockQuote",
                    "indent",
                    "link",
                    "|",
                    "bulletedList",
                    "numberedList",
                ],
            }).catch((error) => {
                console.error(error);
            });
        </script>
    </body>
</html>

As you can notice, the view relies on the nav.ejs partial, a reusable navigation bar that will be located in most views in our app. Therefore, let’s add the views/nav.ejs file.

<!-- views/nav.ejs -->

<nav
    class="border-b border-gray-200 py-5 relative z-20 bg-background shadow-[0_0_15px_0_rgb(0,0,0,0.1)]"
>
    <div class="flex items-center lg:px-6 px-8 mx-auto max-w-7xl">
        <div class="flex-1 hidden md:flex">
            <a href="/" class="text-blue-600 hover:text-blue-800 visited:text-purple-600">Home</a>
        </div>
        <div class="flex-1 justify-end flex items-center md:flex gap-3 h-8">
            <% if (name) { %>
            <span>Welcome, <%= name %>!</span>
            <form action="/logout" method="post">
                <button
                    type="submit"
                    class="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-1.5"
                >
                    Sign out
                </button>
            </form>
            <% } else { %>
            <a
                href="/login"
                class="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-1.5"
                >Sign in</a
            >
            <% } %>
            <a
                href="https://github.com/ckbox-io/ckbox-express-example"
                target="_blank"
                rel="noreferrer"
                class="text-blue-600 hover:text-blue-800 visited:text-purple-600"
            >
                GitHub
            </a>
        </div>
    </div>
</nav>

Then, let’s declare a route for this view in the routes/ckbox.js file.

// routes/ckbox.js

// ...

const ensureLoggedIn = require("connect-ensure-login").ensureLoggedIn;
const PUBLIC_URL = process.env.PUBLIC_URL;

router.get("/ckeditor", ensureLoggedIn(), function (req, res) {
    res.render("ckeditor", {
        name: req.user.name,
        tokenUrl: `${PUBLIC_URL}/api/ckbox`,
    });
});

// ...

Finally, let’s take the opportunity to add the PUBLIC_URL=http://localhost:3000 entry to the .env file.

# CKBox as file picker

One of the common scenarios is to use CKBox as a file picker, where the user can choose one of the files stored in the file manager. After choosing the file, we want to obtain information about the chosen files, especially their URLs. This can be achieved using the assets.onChoose callback passed as the CKBox’s configuration option.

Below is the code snippet for the views/file-picker.ejs view.

<!-- views/file-picker.ejs -->

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <script src="https://cdn.tailwindcss.com"></script>
        <script src="https://cdn.ckbox.io/ckbox/2.4.0/ckbox.js"></script>
        <title>File picker</title>
    </head>
    <body>
        <div class="mx-auto h-screen flex flex-col">
            <%- include('nav'); %>
            <div class="px-8 flex-1 bg-accents-0">
                <main class="w-full max-w-4xl mx-auto py-16 h-full flex flex-col">
                    <section class="flex flex-col gap-6">
                        <h2 class="text-4xl font-semibold tracking-tight">File Picker</h2>
                    </section>
                    <hr class="border-t border-accents-2 my-6" />
                    <section class="flex flex-col gap-3">
                        One of the common scenarios is to use CKBox as a file picker, where the user
                        can choose one of the files stored in the file manager. After choosing the
                        file, we want to obtain information about the chosen files, especially their
                        URLs.
                        <div>
                            <button
                                id="choose-assets"
                                class="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-1.5"
                            >
                                Choose assets
                            </button>
                        </div>
                        <div id="ckbox"></div>
                    </section>
                    <section class="flex flex-col gap-3">
                        <ul id="assets-list"></ul>
                    </section>
                </main>
            </div>
        </div>
        <script>
            const chooseBtn = document.getElementById("choose-assets");
            const assetsList = document.getElementById("assets-list");

            const handleChoose = (assets) => {
                assetsList.innerHTML = "";

                const name = assets.forEach(({ data }) => {
                    const item = document.createElement("li");
                    const name = document.createElement("span");

                    name.textContent = `${data.name}.${data.extension}`;

                    if (data.url) {
                        const link = document.createElement("a");
                        const linkClasses = [
                            "text-blue-600",
                            "hover:text-blue-800",
                            "visited:text-purple-600",
                        ];

                        link.classList.add(...linkClasses);
                        link.setAttribute("href", data.url);
                        link.setAttribute("target", "_blank");
                        link.setAttribute("rel", "noreferrer");

                        link.appendChild(name);
                        item.appendChild(link);
                    } else {
                        item.appendChild(name);
                    }

                    assetsList.appendChild(item);
                });
            };

            chooseBtn.addEventListener("click", () => {
                CKBox.mount(document.getElementById("ckbox"), {
                    tokenUrl: "<%= tokenUrl %>",
                    dialog: true,
                    assets: { onChoose: handleChoose },
                });
            });
        </script>
    </body>
</html>

Then, let’s declare a route for this view in the routes/ckbox.js file.

// routes/ckbox.js

// ...

router.get("/file-picker", ensureLoggedIn(), function (req, res) {
    res.render("file-picker", {
        name: req.user.name,
        tokenUrl: `${PUBLIC_URL}/api/ckbox`,
    });
});

// ...

# CKBox in inline mode

To initialize CKBox in inline mode, you can simply mount it in the desired place of your app. This is the default embedding mode for CKBox, so no additional configuration props besides tokenUrl are required. CKBox will occupy as much space as allowed by its parent element.

Below is the code snippet for the views/inline.ejs view.

<!-- views/inline.ejs -->

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <script src="https://cdn.tailwindcss.com"></script>
        <script src="https://cdn.ckbox.io/ckbox/2.4.0/ckbox.js"></script>
        <title>Inline</title>
    </head>
    <body>
        <div class="mx-auto h-screen flex flex-col">
            <%- include('nav'); %>
            <div class="px-8 flex-1 bg-accents-0">
                <main class="w-full max-w-4xl mx-auto py-16 h-full flex flex-col">
                    <section class="flex flex-col gap-6">
                        <h2 class="text-4xl font-semibold tracking-tight">Inline</h2>
                    </section>
                    <hr class="border-t border-accents-2 my-6" />
                    <section class="flex flex-col gap-3 flex-1">
                        <p>
                            To start CKBox in inline mode, you can instantiate it in an arbitrary
                            container. CKBox will respect height and width of the container.
                        </p>
                        <div id="ckbox" class="h-full"></div>
                    </section>
                </main>
            </div>
        </div>
        <script>
            CKBox.mount(document.getElementById("ckbox"), {
                tokenUrl: "<%= tokenUrl %>",
            });
        </script>
    </body>
</html>

Then, let’s declare a route for this view in the routes/ckbox.js file.

// routes/ckbox.js

// ...

router.get("/inline", ensureLoggedIn(), function (req, res) {
    res.render("inline", {
        name: req.user.name,
        tokenUrl: `${PUBLIC_URL}/api/ckbox`,
    });
});

// ...

# Home page

Finally, let’s tweak the home page so that it displays links to all views created above. Therefore, let’s replace the contents of the initial views/index.ejs file with the snippet outlined below.

<!-- views/index.ejs -->

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <script src="https://cdn.tailwindcss.com"></script>
        <title>Express</title>
    </head>
    <body>
        <div class="mx-auto h-screen flex flex-col">
            <%- include('nav'); %>
            <div class="px-8 flex-1 bg-accents-0">
                <main class="w-full max-w-4xl mx-auto py-16 h-full flex flex-col">
                    <section class="flex flex-col gap-6">
                        <h2 class="text-3xl font-semibold tracking-tight">
                            CKBox integration with Express
                        </h2>
                    </section>
                    <hr class="border-t border-accents-2 my-6" />
                    <section class="flex flex-col gap-3">
                        <p>Below you can find example integrations of CKBox.</p>
                        <p>
                            In a typical scenario access to CKBox will be restricted to
                            authenticated users only. Therefore, each sample is restricted to signed
                            in users only. Use different credentials to unlock various CKBox roles.
                            See available users
                            <a
                                href="https://github.com/ckbox-io/ckbox-express-example/blob/main/routes/auth.js"
                                target="_blank"
                                rel="noreferrer"
                                class="text-blue-600 hover:text-blue-800"
                                >here</a
                            >.
                        </p>
                        <div class="flex-1 hidden md:flex gap-2">
                            <ol>
                                <li>
                                    <a class="text-blue-600 hover:text-blue-800" href="/inline"
                                        >Inline mode</a
                                    >
                                </li>
                                <li>
                                    <a class="text-blue-600 hover:text-blue-800" href="/file-picker"
                                        >File picker</a
                                    >
                                </li>
                                <li>
                                    <a class="text-blue-600 hover:text-blue-800" href="/ckeditor"
                                        >CKEditor</a
                                    >
                                </li>
                            </ol>
                        </div>
                    </section>
                </main>
            </div>
        </div>
    </body>
</html>

Also, let’s adjust the index router by replacing the contents of the routes/index.js file with the following snippet:

const express = require("express");
const router = express.Router();

router.get("/", function (req, res) {
    res.render("index", { name: req.user?.name });
});

module.exports = router;

# App

Finally, let’s combine all routes with the main app.js file. Replace the file’s contents with the snippet below.

const createError = require("http-errors");
const express = require("express");
const path = require("path");
const session = require("express-session");
const cookieParser = require("cookie-parser");
const logger = require("morgan");
const passport = require("passport");

require("dotenv").config();

const indexRouter = require("./routes/index");
const ckboxRouter = require("./routes/ckbox");
const authRouter = require("./routes/auth");

const app = express();

app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");

app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));

app.use(
    session({
        secret: process.env.SESSION_SECRET,
        resave: false,
        saveUninitialized: true,
    })
);
app.use(passport.authenticate("session"));

app.use("/", indexRouter);
app.use("/", authRouter);
app.use("/", ckboxRouter);

app.use(function (req, res, next) {
    next(createError(404));
});

app.use(function (err, req, res, next) {
    res.locals.message = err.message;
    res.locals.error = req.app.get("env") === "development" ? err : {};
    res.status(err.status || 500);
    res.render("error");
});

module.exports = app;

User sessions established with the help of the express-session package are kept in memory. Therefore, you must sign in again anytime the Express app is started.

As you can see, we’ve used one more environment variable: SESSION_SECRET. Its value is used by the express-session package to sign the session ID cookie and would best be a random set of characters. Below is the complete content of the .env file.

# .env
CKBOX_ENVIRONMENT_ID=REPLACE-WITH-ENVIRONMENT-ID
CKBOX_ACCESS_KEY=REPLACE-WITH-ACCESS-KEY
PUBLIC_URL=http://localhost:3000
SESSION_SECRET=KdTwwFg0zOd3Bp4INE2UmnEogLJK5uma1wdYABaaODs=

# Congratulations

Congratulations on completing the guide! You can now access the app by running the following command:

npm start

# Complete code

On GitHub, you can find the complete code of the application described in this guide.